Merge branch 'develop' into debit-credit-opening-invoice-tool

This commit is contained in:
Marica 2020-11-03 12:03:50 +05:30 committed by GitHub
commit 9a75a60784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
232 changed files with 14708 additions and 6241 deletions

View File

@ -101,7 +101,7 @@ class Account(NestedSet):
return
if not frappe.db.get_value("Account",
{'account_name': self.account_name, 'company': ancestors[0]}, 'name'):
frappe.throw(_("Please add the account to root level Company - %s" % ancestors[0]))
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account:
descendants = get_descendants_of('Company', self.company)
if not descendants: return
@ -164,9 +164,19 @@ class Account(NestedSet):
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
for company in descendants:
company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company):
frappe.throw(_("While creating account for child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
.format(company, parent_acc_name))
frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
.format(company_bold, parent_acc_name_bold), title=_("Account Not Found"))
# validate if parent of child company account to be added is a group
if (frappe.db.get_value("Account", self.parent_account, "is_group")
and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")):
msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
msg += "<br><br>"
msg += _("Please convert the parent account in corresponding child company to a group account.")
frappe.throw(msg, title=_("Invalid Parent Account"))
filters = {
"account_name": self.account_name,
@ -309,8 +319,9 @@ def update_account_number(name, account_name, account_number=None, from_descenda
allow_child_account_creation = _("Allow Account Creation Against Child Company")
message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
message += "<br>" + _("Renaming it is only allowed via parent company {0}, \
to avoid mismatch.").format(frappe.bold(ancestor)) + "<br><br>"
message += "<br>"
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
message += "<br><br>"
message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
frappe.throw(message, title=_("Rename Not Allowed"))

View File

@ -111,6 +111,17 @@ class TestAccount(unittest.TestCase):
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
def test_add_account_to_a_group(self):
frappe.db.set_value("Account", "Office Rent - _TC3", "is_group", 1)
acc = frappe.new_doc("Account")
acc.account_name = "Test Group Account"
acc.parent_account = "Office Rent - _TC3"
acc.company = "_Test Company 3"
self.assertRaises(frappe.ValidationError, acc.insert)
frappe.db.set_value("Account", "Office Rent - _TC3", "is_group", 0)
def test_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
@ -160,6 +171,7 @@ class TestAccount(unittest.TestCase):
for doc in to_delete:
frappe.delete_doc("Account", doc)
def _make_test_records(verbose):
from frappe.test_runner import make_test_objects

View File

@ -7,7 +7,7 @@ frappe.ui.form.on('Accounting Dimension', {
frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list;
invalid_doctypes.push('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail');
'Cost Center', 'Accounting Dimension Detail', 'Company');
return {
filters: {

View File

@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail') :
'Cost Center', 'Accounting Dimension Detail', 'Company') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)

View File

@ -51,6 +51,7 @@ frappe.ui.form.on('POS Closing Entry', {
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user
},
callback: (r) => {

View File

@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import
class POSClosingEntry(Document):
def validate(self):
user = frappe.get_all('POS Closing Entry',
filters = { 'user': self.user, 'docstatus': 1 },
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"))
self.validate_pos_closing()
self.validate_pos_invoices()
def validate_pos_closing(self):
user = frappe.get_all("POS Closing Entry",
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
or_filters = {
'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
'period_end_date': ('between', [self.period_start_date, self.period_end_date])
"period_start_date": ("between", [self.period_start_date, self.period_end_date]),
"period_end_date": ("between", [self.period_start_date, self.period_end_date])
})
if user:
frappe.throw(_("POS Closing Entry {} against {} between selected period"
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
bold_already_exists = frappe.bold(_("already exists"))
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
invalid_row = {'idx': d.idx}
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
if pos_invoice.consolidated_invoice:
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
invalid_rows.append(invalid_row)
continue
if pos_invoice.pos_profile != self.pos_profile:
invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)))
if pos_invoice.docstatus != 1:
invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted")))
if pos_invoice.owner != self.user:
invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)))
if invalid_row.get('msg'):
invalid_rows.append(invalid_row)
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)
def on_submit(self):
merge_pos_invoices(self.pos_transactions)
@ -47,16 +79,15 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
return [c['user'] for c in cashiers_list]
@frappe.whitelist()
def get_pos_invoices(start, end, user):
def get_pos_invoices(start, end, pos_profile, user):
data = frappe.db.sql("""
select
name, timestamp(posting_date, posting_time) as "timestamp"
from
`tabPOS Invoice`
where
owner = %s and docstatus = 1 and
(consolidated_invoice is NULL or consolidated_invoice = '')
""", (user), as_dict=1)
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
""", (user, pos_profile), as_dict=1)
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
# need to get taxes and payments so can't avoid get_doc
@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 0
closing_entry.total_quantity = 0
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user)
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date,
closing_entry.pos_profile, closing_entry.user)
pos_transactions = []
taxes = []

View File

@ -7,8 +7,8 @@
"field_order": [
"mode_of_payment",
"opening_amount",
"closing_amount",
"expected_amount",
"closing_amount",
"difference"
],
"fields": [
@ -26,8 +26,7 @@
"in_list_view": 1,
"label": "Expected Amount",
"options": "company:company_currency",
"read_only": 1,
"reqd": 1
"read_only": 1
},
{
"fieldname": "difference",
@ -55,9 +54,10 @@
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-05-29 15:03:34.533607",
"modified": "2020-10-23 16:45:43.662034",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry Detail",

View File

@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc);
},
onload() {
onload(doc) {
this._super();
if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
//Load pos profile data on the invoice if the default value of Is POS is 1
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
}
},
refresh(doc) {
this._super();
if (doc.docstatus == 1 && !doc.is_return) {
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
cur_frm.add_custom_button(__('Return'),
this.make_sales_return, __('Create'));
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if (this.frm.doc.is_return) {
if (doc.is_return && doc.__islocal) {
this.frm.return_print_format = "Sales Invoice Return";
cur_frm.set_value('consolidated_invoice', '');
this.frm.set_value('consolidated_invoice', '');
}
},
is_pos: function(frm){
is_pos: function() {
this.set_pos_data();
},
set_pos_data: function() {
set_pos_data: async function() {
if(this.frm.doc.is_pos) {
this.frm.set_value("allocate_advances_automatically", 0);
if(!this.frm.doc.company) {
this.frm.set_value("is_pos", 0);
frappe.msgprint(__("Please specify Company to proceed"));
} else {
var me = this;
return this.frm.call({
doc: me.frm.doc,
const r = await this.frm.call({
doc: this.frm.doc,
method: "set_missing_values",
callback: function(r) {
freeze: true
});
if(!r.exc) {
if(r.message) {
me.frm.pos_print_format = r.message.print_format || "";
me.frm.meta.default_print_format = r.message.print_format || "";
me.frm.allow_edit_rate = r.message.allow_edit_rate;
me.frm.allow_edit_discount = r.message.allow_edit_discount;
me.frm.doc.campaign = r.message.campaign;
me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
this.frm.pos_print_format = r.message.print_format || "";
this.frm.meta.default_print_format = r.message.print_format || "";
this.frm.doc.campaign = r.message.campaign;
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
}
me.frm.script_manager.trigger("update_stock");
me.calculate_taxes_and_totals();
if(me.frm.doc.taxes_and_charges) {
me.frm.script_manager.trigger("taxes_and_charges");
}
frappe.model.set_default_values(me.frm.doc);
me.set_dynamic_labels();
this.frm.script_manager.trigger("update_stock");
this.calculate_taxes_and_totals();
this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges");
frappe.model.set_default_values(this.frm.doc);
this.set_dynamic_labels();
}
}
});
}
}
else this.frm.trigger("refresh");
},
customer() {
if (!this.frm.doc.customer) return
if (this.frm.doc.is_pos){
var pos_profile = this.frm.doc.pos_profile;
}
var me = this;
const pos_profile = this.frm.doc.pos_profile;
if(this.frm.updating_party_details) return;
erpnext.utils.get_party_details(this.frm,
"erpnext.accounts.party.get_party_details", {
@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile
}, function() {
me.apply_pricing_rule();
}, () => {
this.apply_pricing_rule();
});
},

View File

@ -10,12 +10,10 @@ from erpnext.controllers.selling_controller import SellingController
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, validate_loyalty_points
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from 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
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option, get_mode_of_payment_info
from six import iteritems
@ -30,8 +28,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller
super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time()
self.validate_pos_paid_amount()
self.validate_pos_return()
self.validate_mode_of_payment()
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
self.validate_debit_to_acc()
@ -41,11 +38,11 @@ class POSInvoice(SalesInvoice):
self.validate_item_cost_centers()
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items()
self.validate_return_items_qty()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
self.verify_payment_amount()
self.validate_payment_amount()
self.validate_loyalty_transaction()
def on_submit(self):
@ -84,70 +81,98 @@ class POSInvoice(SalesInvoice):
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
if self.is_return:
return
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
error_msg = []
for d in self.get('items'):
msg = ""
if d.serial_no:
filters = {
"item_code": d.item_code,
"warehouse": d.warehouse,
"delivery_document_no": "",
"sales_invoice": ""
}
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = d.serial_no.split("\n")
serial_nos = ' '.join(serial_nos).split() # remove whitespaces
invalid_serial_nos = []
for s in serial_nos:
if s in reserved_serial_nos:
invalid_serial_nos.append(s)
if len(invalid_serial_nos):
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format(
d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available"))
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
else:
if allow_negative_stock:
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
if not (flt(available_stock) > 0):
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format(
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available"))
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format(
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available"))
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
def validate_serialised_or_batched_item(self):
error_msg = []
for d in self.get("items"):
serialized = d.get("has_serial_no")
batched = d.get("has_batch_no")
no_serial_selected = not d.get("serial_no")
no_batch_selected = not d.get("batch_no")
msg = ""
item_code = frappe.bold(d.item_code)
if serialized and batched and (no_batch_selected or no_serial_selected):
frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.').format(
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
.format(d.idx, item_code))
if serialized and no_serial_selected:
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.').format(
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
.format(d.idx, item_code))
if batched and no_batch_selected:
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.').format(
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
.format(d.idx, item_code))
if msg:
error_msg.append(msg)
def validate_return_items(self):
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_return_items_qty(self):
if not self.get("is_return"): return
for d in self.get("items"):
if d.get("qty") > 0:
frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
frappe.throw(
_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")
)
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
serial_no_exists = frappe.db.exists("POS Invoice Item", {
"parent": self.return_against,
"serial_no": ["like", d.get("serial_no")]
})
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
frappe.throw(
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos:
def validate_mode_of_payment(self):
if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_change_account(self):
@ -165,20 +190,18 @@ class POSInvoice(SalesInvoice):
if flt(self.change_amount) and not self.account_for_change_amount:
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
def verify_payment_amount(self):
def validate_payment_amount(self):
total_amount_in_payments = 0
for entry in self.payments:
total_amount_in_payments += entry.amount
if not self.is_return and entry.amount < 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self):
if self.is_pos and self.is_return:
total_amount_in_payments = 0
for payment in self.payments:
total_amount_in_payments += payment.amount
if self.is_return:
invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total:
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_loyalty_transaction(self):
@ -233,55 +256,45 @@ class POSInvoice(SalesInvoice):
pos_profile = get_pos_profile(self.company) or {}
self.pos_profile = pos_profile.get('name')
pos = {}
profile = {}
if self.pos_profile:
pos = frappe.get_doc('POS Profile', self.pos_profile)
profile = frappe.get_doc('POS Profile', self.pos_profile)
if not self.get('payments') and not for_validate:
update_multi_mode_option(self, pos)
update_multi_mode_option(self, profile)
if not self.account_for_change_amount:
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
if pos:
if not for_validate:
self.tax_category = pos.get("tax_category")
if self.is_return and not for_validate:
add_return_modes(self, profile)
if profile:
if not for_validate and not self.customer:
self.customer = pos.customer
self.customer = profile.customer
self.ignore_pricing_rule = pos.ignore_pricing_rule
if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount')
if pos.get('warehouse'):
self.set_warehouse = pos.get('warehouse')
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
for fieldname in ('currency', 'letter_head', 'tc_name',
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)):
self.set(fieldname, pos.get(fieldname))
if pos.get("company_address"):
self.company_address = pos.get("company_address")
'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
'ignore_pricing_rule', 'company_address', 'update_stock'):
if not for_validate:
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_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
else:
selling_price_list = pos.get('selling_price_list')
selling_price_list = profile.get('selling_price_list')
if selling_price_list:
self.set('selling_price_list', selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
if item.get('item_code'):
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@ -294,10 +307,13 @@ class POSInvoice(SalesInvoice):
if self.taxes_and_charges and not len(self.get("taxes")):
self.set_taxes()
return pos
if not self.account_for_change_amount:
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
return profile
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
profile = self.set_pos_fields(for_validate)
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
@ -307,17 +323,15 @@ class POSInvoice(SalesInvoice):
super(SalesInvoice, self).set_missing_values(for_validate)
print_format = pos.get("print_format") if pos else None
print_format = profile.get("print_format") if profile else None
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
print_format = 'POS Invoice'
if pos:
if profile:
return {
"print_format": print_format,
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
"campaign": pos.get("campaign"),
"allow_print_before_pay": pos.get("allow_print_before_pay")
"campaign": profile.get("campaign"),
"allow_print_before_pay": profile.get("allow_print_before_pay")
}
def set_account_for_mode_of_payment(self):
@ -373,11 +387,9 @@ def get_stock_availability(item_code, warehouse):
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty
else:
# when sle_qty is 0
# when sle_qty > 0 and pos_sales_qty is 0
return sle_qty
@frappe.whitelist()
@ -411,3 +423,18 @@ def make_merge_log(invoices):
if merge_log.get('pos_invoices'):
return merge_log.as_dict()
def add_return_modes(doc, pos_profile):
def append_payment(payment_mode):
payment = doc.append('payments', {})
payment.default = payment_mode.default
payment.mode_of_payment = payment_mode.parent
payment.account = payment_mode.default_account
payment.type = payment_mode.type
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
mode_of_payment = pos_payment_method.mode_of_payment
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])

View File

@ -27,17 +27,24 @@ class POSInvoiceMergeLog(Document):
status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
if status == "Consolidated":
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]:
bold_return_against = frappe.bold(return_against)
return_against_status = frappe.db.get_value('POS Invoice', return_against, "status")
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
frappe.throw(
_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
You can add original invoice {} manually to proceed.")
.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
)
bold_unconsolidated = frappe.bold("not Consolidated")
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
msg += _("Original invoice should be consolidated before or along with the return invoice.")
msg += "<br><br>"
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
frappe.throw(msg)
def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]

View File

@ -17,18 +17,25 @@ class POSOpeningEntry(StatusUpdater):
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company)))
frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company))
if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
def validate_payment_method_account(self):
invalid_modes = []
for d in self.balance_details:
account = frappe.db.get_value("Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_submit(self):
self.set_status(update=True)

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"default",
"allow_in_returns",
"mode_of_payment"
],
"fields": [
@ -24,11 +25,19 @@
"label": "Mode of Payment",
"options": "Mode of Payment",
"reqd": 1
},
{
"default": "0",
"fieldname": "allow_in_returns",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allow In Returns"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-05-29 15:08:41.704844",
"modified": "2020-10-20 12:58:46.114456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Payment Method",

View File

@ -14,6 +14,7 @@
"column_break_9",
"update_stock",
"ignore_pricing_rule",
"hide_unavailable_items",
"warehouse",
"campaign",
"company_address",
@ -290,28 +291,36 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory_depends_on": "update_stock",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock"
"options": "Warehouse",
"reqd": 1
},
{
"default": "0",
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule"
},
{
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
"read_only": 1
},
{
"default": "0",
"fieldname": "hide_unavailable_items",
"fieldtype": "Check",
"label": "Hide Unavailable Items"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-01 17:29:27.759088",
"modified": "2020-10-29 13:18:38.795925",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -56,19 +56,29 @@ class POSProfile(Document):
if not self.payments:
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
default_mode_of_payment = [d.default for d in self.payments if d.default]
if not default_mode_of_payment:
default_mode = [d.default for d in self.payments if d.default]
if not default_mode:
frappe.throw(_("Please select a default mode of payment"))
if len(default_mode_of_payment) > 1:
if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
invalid_modes = []
for d in self.payments:
account = frappe.db.get_value("Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
account = frappe.db.get_value(
"Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
"default_account"
)
if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_update(self):
self.set_defaults()

View File

@ -9,8 +9,7 @@ frappe.ui.form.on('POS Settings', {
get_invoice_fields: function(frm) {
frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
['Table', 'Button'].includes(d.fieldtype)) {
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) {
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
} else {
return null;

View File

@ -504,10 +504,10 @@
},
{
"default": "0",
"depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules",
"depends_on": "eval:in_list(['Discount Percentage'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules",
"fieldname": "apply_discount_on_rate",
"fieldtype": "Check",
"label": "Apply Discount on Rate"
"label": "Apply Discount on Discounted Rate"
},
{
"default": "0",
@ -563,7 +563,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2020-08-26 12:24:44.740734",
"modified": "2020-10-28 16:53:14.416172",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -60,6 +60,15 @@ class PricingRule(Document):
if self.price_or_product_discount == 'Price' and not self.rate_or_discount:
throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError)
if self.apply_discount_on_rate:
if not self.priority:
throw(_("As the field {0} is enabled, the field {1} is mandatory.")
.format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
if self.priority and cint(self.priority) == 1:
throw(_("As the field {0} is enabled, the value of the field {1} should be more than 1.")
.format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
def validate_applicable_for_selling_or_buying(self):
if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected"))
@ -226,12 +235,11 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
item_details = frappe._dict({
"doctype": args.doctype,
"has_margin": False,
"name": args.name,
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get('child_docname'),
"discount_percentage_on_rate": [],
"discount_amount_on_rate": []
"child_docname": args.get('child_docname')
})
if args.ignore_pricing_rule or not args.item_code:
@ -279,6 +287,10 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
else:
get_product_discount_rule(pricing_rule, item_details, args, doc)
if not item_details.get("has_margin"):
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
item_details.has_pricing_rule = 1
item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules])
@ -330,13 +342,11 @@ def get_pricing_rule_details(args, pricing_rule):
def apply_price_discount_rule(pricing_rule, item_details, args):
item_details.pricing_rule_for = pricing_rule.rate_or_discount
if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency)
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
or (pricing_rule.margin_type == 'Percentage')):
item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else:
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
item_details.has_margin = True
if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0
@ -351,9 +361,9 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.rate_or_discount != apply_on: continue
field = frappe.scrub(apply_on)
if pricing_rule.apply_discount_on_rate:
discount_field = "{0}_on_rate".format(field)
item_details[discount_field].append(pricing_rule.get(field, 0))
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
# Apply discount on discounted rate
item_details[field] += ((100 - item_details[field]) * (pricing_rule.get(field, 0) / 100))
else:
if field not in item_details:
item_details.setdefault(field, 0)
@ -361,14 +371,6 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.get(field, 0)
if pricing_rule else args.get(field, 0))
def set_discount_amount(rate, item_details):
for field in ['discount_percentage_on_rate', 'discount_amount_on_rate']:
for d in item_details.get(field):
dis_amount = (rate * d / 100
if field == 'discount_percentage_on_rate' else d)
rate -= dis_amount
item_details.rate = rate
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules,
get_pricing_rule_items)

View File

@ -457,6 +457,33 @@ class TestPricingRule(unittest.TestCase):
item = si.items[0]
self.assertEquals(item.rate, 900)
def test_multiple_pricing_rules(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
title="_Test Pricing Rule 1")
make_pricing_rule(discount_percentage=10, selling=1, title="_Test Pricing Rule 2", priority=2,
apply_multiple_pricing_rules=1)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def test_multiple_pricing_rules_with_apply_discount_on_discounted_rate(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
title="_Test Pricing Rule 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2,
apply_discount_on_rate=1, title="_Test Pricing Rule 2", apply_multiple_pricing_rules=1)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 28)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def make_pricing_rule(**args):
args = frappe._dict(args)
@ -468,6 +495,7 @@ def make_pricing_rule(**args):
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
"currency": "USD",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,
"max_qty": args.max_qty or 0.0,
@ -476,9 +504,13 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or ''
"condition": args.condition or '',
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
if args.get("priority"):
doc.priority = args.get("priority")
apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
doc.append(child_table.get(doc.apply_on), {

View File

@ -14,9 +14,8 @@ import frappe
from erpnext.setup.doctype.item_group.item_group import get_child_item_groups
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.get_item_details import get_conversion_factor
from frappe import _, throw
from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today
from frappe import _, bold
from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
class MultiplePricingRuleConflict(frappe.ValidationError): pass
@ -42,6 +41,7 @@ def get_pricing_rules(args, doc=None):
if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules):
pricing_rules = sorted_by_priority(pricing_rules)
for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule:
@ -53,6 +53,20 @@ def get_pricing_rules(args, doc=None):
return rules
def sorted_by_priority(pricing_rules):
# If more than one pricing rules, then sort by priority
pricing_rules_list = []
pricing_rule_dict = {}
for pricing_rule in pricing_rules:
if not pricing_rule.get("priority"): continue
pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
for key in sorted(pricing_rule_dict):
pricing_rules_list.append(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []
if doc:
@ -284,12 +298,13 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr
fieldname = field
if fieldname:
msg = _("""If you {0} {1} quantities of the item <b>{2}</b>, the scheme <b>{3}</b>
will be applied on the item.""").format(type_of_transaction, args.get(fieldname), item_code, args.rule_description)
msg = (_("If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item.")
.format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)))
if fieldname in ['min_amt', 'max_amt']:
msg = _("""If you {0} {1} worth item <b>{2}</b>, the scheme <b>{3}</b> will be applied on the item.
""").format(frappe.fmt_money(type_of_transaction, args.get(fieldname)), item_code, args.rule_description)
msg = (_("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.")
.format(type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")),
bold(item_code), bold(args.rule_description)))
frappe.msgprint(msg)

View File

@ -156,14 +156,16 @@ class PurchaseInvoice(BuyingController):
["account_type", "report_type", "account_currency"], as_dict=True)
if account.report_type != "Balance Sheet":
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
You can change the parent account to a Balance Sheet account or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account"))
frappe.throw(
_("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account")
)
if self.supplier and account.account_type != "Payable":
frappe.throw(_("Please ensure {} account is a Payable account. \
Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account"))
frappe.throw(
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account")
)
self.party_account_currency = account.account_currency
@ -249,10 +251,10 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2}
is not linked to warehouse {3} or it is not the default inventory account'''.format(
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]),
frappe.bold(item.expense_account), frappe.bold(item.warehouse))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
msg += _("or it is not the default inventory account")
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
@ -264,19 +266,19 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because
expense is booked against this account in Purchase Receipt {2}'''.format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase
Receipt is created against Item {2}. This is done to handle accounting for cases
when Purchase Receipt is created after Purchase Invoice'''.format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@ -304,10 +306,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_order:
throw(_("""Purchase Order Required for item {0}
To submit the invoice without purchase order please set
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')),
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
stock_items = self.get_stock_items()
@ -318,10 +321,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_receipt and d.item_code in stock_items:
throw(_("""Purchase Receipt Required for item {0}
To submit the invoice without purchase receipt please set
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')),
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
if self.write_off_amount and not self.write_off_account:

View File

@ -484,14 +484,14 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
You can change the parent account to a Balance Sheet account or select a different account.")
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
frappe.throw(_("Please ensure {} account is a Receivable account. \
Change the account type to Receivable or select a different account.")
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
self.party_account_currency = account.account_currency
@ -1146,8 +1146,10 @@ class SalesInvoice(SellingController):
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
if against_lp_entry:
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
frappe.throw(
_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
.format(self.doctype, self.doctype, invoice_list)
)
else:
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
# Set loyalty program
@ -1618,17 +1620,25 @@ def update_multi_mode_option(doc, pos_profile):
payment.type = payment_mode.type
doc.set('payments', [])
invalid_modes = []
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
if not payment_mode:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account"))
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
continue
payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0])
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def get_all_mode_of_payments(doc):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type

View File

@ -237,7 +237,7 @@ class TestSubscription(unittest.TestCase):
subscription.party_type = 'Customer'
subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.start_date = '2018-01-01'
subscription.start_date = add_days(nowdate(), -1000)
subscription.insert()
subscription.process() # generate first invoice

View File

@ -307,7 +307,7 @@ def get_accounts(company, root_type):
where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True)
def filter_accounts(accounts, depth=10):
def filter_accounts(accounts, depth=20):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:

View File

@ -63,6 +63,7 @@ def get_pos_entries(filters, group_by_field):
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
WHERE
p.docstatus = 1 and
{group_by_mop_condition}
{conditions}
ORDER BY

View File

@ -9,9 +9,9 @@
"filters_json": "{\"status\":\"In Location\",\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"date_based_on\":\"Purchase Date\",\"group_by\":\"--Select a group--\"}",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-07-23 13:53:33.211371",
"modified": "2020-10-28 23:15:58.432189",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Value Analytics",

View File

@ -8,9 +8,9 @@
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"is_existing_asset\":0}",
"idx": 0,
"is_public": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-07-23 13:39:32.429240",
"modified": "2020-10-28 23:16:16.939070",
"modified_by": "Administrator",
"module": "Assets",
"name": "Category-wise Asset Value",

View File

@ -8,9 +8,9 @@
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"is_existing_asset\":0}",
"idx": 0,
"is_public": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-07-23 13:42:44.912551",
"modified": "2020-10-28 23:16:07.883312",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location-wise Asset Value",

View File

@ -55,6 +55,7 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Depreciation Posting Date",
"mandatory_depends_on": "eval:parent.doctype == 'Asset'",
"reqd": 1
},
{
@ -86,7 +87,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-16 12:11:30.631788",
"modified": "2020-10-30 15:22:29.119868",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@ -19,6 +19,8 @@ from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@ -686,7 +688,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code)
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1)
@ -708,7 +710,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code)
make_subcontracted_item(item_code=item_code)
make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
@ -767,6 +769,133 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM")
def test_backflushed_based_on_for_multiple_batches(self):
item_code = "_Test Subcontracted FG Item 2"
make_item('Sub Contracted Raw Material 2', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
raw_materials=["Sub Contracted Raw Material 2"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 500
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
make_new_batch(batch_id=batch, item_code=item_code)
pr = make_purchase_receipt(po.name)
# partial receipt
pr.get('items')[0].qty = 30
pr.get('items')[0].batch_no = "ABCD1"
purchase_order = po.name
purchase_order_item = po.items[0].name
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
pr.append("items", {
"item_code": pr.get('items')[0].item_code,
"item_name": pr.get('items')[0].item_name,
"uom": pr.get('items')[0].uom,
"stock_uom": pr.get('items')[0].stock_uom,
"warehouse": pr.get('items')[0].warehouse,
"conversion_factor": pr.get('items')[0].conversion_factor,
"cost_center": pr.get('items')[0].cost_center,
"rate": pr.get('items')[0].rate,
"qty": qty,
"batch_no": batch_no,
"purchase_order": purchase_order,
"purchase_order_item": purchase_order_item
})
pr.submit()
pr1 = make_purchase_receipt(po.name)
pr1.get('items')[0].qty = 300
pr1.get('items')[0].batch_no = "ABCD1"
pr1.save()
pr_key = ("Sub Contracted Raw Material 2", po.name)
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
item_code = "_Test Subcontracted FG Item 5"
make_item('Sub Contracted Raw Material 4', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 250
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
# Add same subcontracted items multiple times
po.append("items", {
"item_code": item_code,
"qty": order_qty,
"schedule_date": add_days(nowdate(), 1),
"warehouse": "_Test Warehouse - _TC"
})
po.set_missing_values()
po.submit()
# Material receipt entry for the raw materials which will be send to supplier
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
rm_items = [
{
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
},
{
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"
},
]
# Raw Materials transfer entry from stores to supplier's warehouse
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
# Test po_detail field has value or not
for item_row in se.items:
self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name)
po_doc = frappe.get_doc("Purchase Order", po.name)
for row in po_doc.supplied_items:
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
self.assertEqual(row.supplied_qty, 250.0)
update_backflush_based_on("BOM")
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings",
@ -839,15 +968,19 @@ def make_pr_against_po(po, received_qty=0):
pr.submit()
return pr
def make_subcontracted_item(item_code):
def make_subcontracted_item(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
if not frappe.db.exists('Item', item_code):
make_item(item_code, {
args = frappe._dict(args)
if not frappe.db.exists('Item', args.item_code):
make_item(args.item_code, {
'is_stock_item': 1,
'is_sub_contracted_item': 1
'is_sub_contracted_item': 1,
'has_batch_no': args.get("has_batch_no") or 0
})
if not args.raw_materials:
if not frappe.db.exists('Item', "Test Extra Item 1"):
make_item("Test Extra Item 1", {
'is_stock_item': 1,
@ -858,8 +991,10 @@ def make_subcontracted_item(item_code):
'is_stock_item': 1,
})
if not frappe.db.get_value('BOM', {'item': item_code}, 'name'):
make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1'])
args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
def update_backflush_based_on(based_on):
doc = frappe.get_doc('Buying Settings')

View File

@ -263,6 +263,7 @@ class AccountsController(TransactionBase):
if self.doctype == "Quotation" and self.quotation_to == "Customer" and parent_dict.get("party_name"):
parent_dict.update({"customer": parent_dict.get("party_name")})
self.pricing_rules = []
for item in self.get("items"):
if item.get("item_code"):
args = parent_dict.copy()
@ -301,6 +302,7 @@ class AccountsController(TransactionBase):
if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret)
if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate)
@ -322,6 +324,9 @@ class AccountsController(TransactionBase):
if item.get('discount_amount'):
item.rate = item.price_list_rate - item.discount_amount
if item.get("apply_discount_on_discounted_rate") and pricing_rule_args.get("rate"):
item.rate = pricing_rule_args.get("rate")
elif pricing_rule_args.get('free_item_data'):
apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data'))
@ -335,6 +340,18 @@ class AccountsController(TransactionBase):
frappe.msgprint(_("Row {0}: user has not applied the rule {1} on the item {2}")
.format(item.idx, frappe.bold(title), frappe.bold(item.item_code)))
def set_pricing_rule_details(self, item_row, args):
pricing_rules = get_applied_pricing_rules(args.get("pricing_rules"))
if not pricing_rules: return
for pricing_rule in pricing_rules:
self.append("pricing_rules", {
"pricing_rule": pricing_rule,
"item_code": item_row.item_code,
"child_docname": item_row.name,
"rule_applied": True
})
def set_taxes(self):
if not self.meta.get_field("taxes"):
return
@ -961,8 +978,10 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
company_currency = frappe.get_cached_value('Company', company, "default_currency")
if not conversion_rate:
throw(_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
conversion_rate_label, currency, company_currency))
throw(
_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.")
.format(conversion_rate_label, currency, company_currency)
)
def validate_taxes_and_charges(tax):

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate
from six import iteritems
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@ -112,8 +112,8 @@ class BuyingController(StockController):
"docstatus": 1
})]
if self.is_return and len(not_cancelled_asset):
frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.".format(self.return_against)),
title=_("Not Allowed"))
frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.")
.format(self.return_against), title=_("Not Allowed"))
def get_asset_items(self):
if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
@ -298,10 +298,10 @@ class BuyingController(StockController):
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
@ -330,8 +330,10 @@ class BuyingController(StockController):
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map)
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
@ -343,6 +345,10 @@ class BuyingController(StockController):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
rm.main_item_code = fg_item_doc.item_code
rm.reference_name = fg_item_doc.name
rm.required_qty = qty
rm.consumed_qty = qty
@ -792,8 +798,8 @@ class BuyingController(StockController):
asset.set(field, None)
asset.supplier = None
if asset.docstatus == 1 and delete_asset:
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\
Please cancel the it to continue.').format(frappe.utils.get_link_to_form('Asset', asset.name)))
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.')
.format(frappe.utils.get_link_to_form('Asset', asset.name)))
asset.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_mandatory = True
@ -873,7 +879,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND sed.subcontracted_item = %s
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
@ -890,39 +896,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
common_query = """
SELECT
CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
SUM(prsi.consumed_qty) AS qty,
{serial_no_concat_syntax} AS serial_nos,
{batch_no_concat_syntax} AS batch_nos
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
WHERE
pr.name = pri.parent
AND pr.name = prsi.parent
AND pri.purchase_order IN %s
AND pri.item_code = prsi.main_item_code
AND pr.docstatus = 1
GROUP BY prsi.rm_item_code, pri.purchase_order
"""
purchase_receipts = frappe.get_all("Purchase Receipt Item",
fields = ["purchase_order", "item_code", "name", "parent"],
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
backflushed_raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
)
}, (purchase_orders, ), as_dict=1)
distinct_purchase_receipts = {}
for pr in purchase_receipts:
key = (pr.purchase_order, pr.item_code, pr.parent)
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
for item in backflushed_raw_materials:
backflushed_raw_materials_map.setdefault(item.item_key, item)
for args, references in iteritems(distinct_purchase_receipts):
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
for data in purchase_receipt_supplied_items:
pr_key = (data.rm_item_code, data.main_item_code, args[0])
if pr_key not in backflushed_raw_materials_map:
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
"qty": 0.0,
"serial_no": [],
"batch_no": [],
"consumed_batch": {}
}))
row = backflushed_raw_materials_map.get(pr_key)
row.qty += data.consumed_qty
for field in ["serial_no", "batch_no"]:
if data.get(field):
row[field].append(data.get(field))
if data.get("batch_no"):
if data.get("batch_no") in row.consumed_batch:
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
else:
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
def get_supplied_items(item_code, purchase_receipt, references):
return frappe.get_all("Purchase Receipt Item Supplied",
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@ -1004,14 +1020,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code
sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND sed.subcontracted_item = %s
AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
@ -1019,8 +1036,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, {})
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
@ -1057,10 +1076,11 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []

View File

@ -608,14 +608,17 @@ class calculate_taxes_and_totals(object):
base_rate_with_margin = 0.0
if item.price_list_rate:
if item.pricing_rules and not self.doc.ignore_pricing_rule:
has_margin = False
for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\
if (pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == self.doc.currency)\
or (pricing_rule.margin_type == 'Percentage'):
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else:
has_margin = True
if not has_margin:
item.margin_type = None
item.margin_rate_or_amount = 0.0

View File

@ -49,6 +49,22 @@ data = {
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
'insert_after': 'reference_dt'
}
],
'Stock Entry': [
{
'fieldname': 'inpatient_medication_entry', 'label': 'Inpatient Medication Entry', 'fieldtype': 'Link', 'options': 'Inpatient Medication Entry',
'insert_after': 'credit_note', 'read_only': True
}
],
'Stock Entry Detail': [
{
'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient',
'insert_after': 'po_detail', 'read_only': True
},
{
'fieldname': 'inpatient_medication_entry_child', 'label': 'Inpatient Medication Entry Child', 'fieldtype': 'Data',
'insert_after': 'patient', 'read_only': True
}
]
},
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'

View File

@ -7,7 +7,7 @@ import frappe
import json
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt, cstr
from frappe.utils import flt, cstr, getdate
from frappe.email.doctype.email_group.email_group import add_subscribers
def get_course(program):
@ -67,6 +67,13 @@ def mark_attendance(students_present, students_absent, course_schedule=None, stu
:param date: Date.
"""
if student_group:
academic_year = frappe.db.get_value('Student Group', student_group, 'academic_year')
if academic_year:
year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
if getdate(date) < getdate(year_start_date) or getdate(date) > getdate(year_end_date):
frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
present = json.loads(students_present)
absent = json.loads(students_absent)

View File

@ -30,6 +30,23 @@ frappe.ui.form.on('Assessment Plan', {
frappe.set_route('Form', 'Assessment Result Tool');
}, __('Tools'));
}
frm.set_query('course', function() {
return {
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
filters: {
'program': frm.doc.program
}
};
});
frm.set_query('academic_term', function() {
return {
filters: {
'academic_year': frm.doc.academic_year
}
};
});
},
course: function(frm) {

View File

@ -12,8 +12,8 @@
"assessment_group",
"grading_scale",
"column_break_2",
"course",
"program",
"course",
"academic_year",
"academic_term",
"section_break_5",
@ -198,7 +198,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-05-09 14:56:26.746988",
"modified": "2020-10-23 15:55:35.076251",
"modified_by": "Administrator",
"module": "Education",
"name": "Assessment Plan",

View File

@ -7,6 +7,23 @@ frappe.ui.form.on('Assessment Result', {
frm.trigger('setup_chart');
}
frm.set_df_property('details', 'read_only', 1);
frm.set_query('course', function() {
return {
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
filters: {
'program': frm.doc.program
}
};
});
frm.set_query('academic_term', function() {
return {
filters: {
'academic_year': frm.doc.academic_year
}
};
});
},
onload: function(frm) {

View File

@ -41,5 +41,24 @@ frappe.ui.form.on("Instructor", {
}
};
});
frm.set_query("academic_term", "instructor_log", function(_doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: {
"academic_year": d.academic_year
}
};
});
frm.set_query("course", "instructor_log", function(_doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses",
filters: {
"program": d.program
}
};
});
}
});

View File

@ -1,336 +1,88 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2017-12-27 08:55:52.680284",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"academic_year",
"academic_term",
"department",
"column_break_3",
"program",
"course",
"student_group",
"section_break_8",
"other_details"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "academic_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Academic Year",
"length": 0,
"no_copy": 0,
"options": "Academic Year",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "academic_term",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Academic Term",
"length": 0,
"no_copy": 0,
"options": "Academic Term",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Academic Term"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Department"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "program",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Program",
"length": 0,
"no_copy": 0,
"options": "Program",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "course",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Course",
"length": 0,
"no_copy": 0,
"options": "Course",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Course"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "student_group",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Student Group",
"length": 0,
"no_copy": 0,
"options": "Student Group",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Student Group"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "other_details",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Other details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Other details"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-11-04 03:38:30.902942",
"links": [],
"modified": "2020-10-23 15:15:50.759657",
"modified_by": "Administrator",
"module": "Education",
"name": "Instructor Log",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -6,13 +6,13 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_link_to_form
from frappe.utils import get_link_to_form, getdate
from erpnext.education.api import get_student_group_students
class StudentAttendance(Document):
def validate(self):
self.validate_mandatory()
self.validate_date()
self.set_date()
self.set_student_group()
self.validate_student()
@ -27,6 +27,18 @@ class StudentAttendance(Document):
frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'),
frappe.bold('Course Schedule')), title=_('Mandatory Fields'))
def validate_date(self):
if not self.leave_application and getdate(self.date) > getdate():
frappe.throw(_('Attendance cannot be marked for future dates.'))
if self.student_group:
academic_year = frappe.db.get_value('Student Group', self.student_group, 'academic_year')
if academic_year:
year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
if year_start_date and year_end_date:
if getdate(self.date) < getdate(year_start_date) or getdate(self.date) > getdate(year_end_date):
frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
def set_student_group(self):
if self.course_schedule:
self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group')
@ -63,6 +75,6 @@ class StudentAttendance(Document):
})
if attendance_record:
record = get_link_to_form('Attendance Record', attendance_record)
record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.format(record, frappe.bold(self.student)), title=_('Duplicate Entry'))

View File

@ -52,6 +52,8 @@ frappe.ui.form.on('Student Attendance Tool', {
},
date: function(frm) {
if (frm.doc.date > frappe.datetime.get_today())
frappe.throw(__("Cannot mark attendance for future dates."));
frm.trigger("student_group");
},
@ -133,8 +135,8 @@ education.StudentsEditor = Class.extend({
return !stud.disabled && !stud.checked;
});
frappe.confirm(__("Do you want to update attendance?<br>Present: {0}\
<br>Absent: {1}", [students_present.length, students_absent.length]),
frappe.confirm(__("Do you want to update attendance? <br> Present: {0} <br> Absent: {1}",
[students_present.length, students_absent.length]),
function() { //ifyes
if(!frappe.request.ajax_count) {
frappe.call({

View File

@ -1,333 +1,118 @@
{
"actions": [],
"allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-11-16 17:12:46.437539",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"based_on",
"group_based_on",
"column_break_2",
"student_group",
"academic_year",
"academic_term",
"course_schedule",
"date",
"attendance",
"students_html"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "based_on",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Based On",
"length": 0,
"no_copy": 0,
"options": "Student Group\nCourse Schedule",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "Student Group\nCourse Schedule"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Batch",
"depends_on": "eval:doc.based_on == \"Student Group\"",
"fieldname": "group_based_on",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Group Based On",
"length": 0,
"no_copy": 0,
"options": "Batch\nCourse\nActivity",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "Batch\nCourse\nActivity"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.based_on ==\"Student Group\"",
"fieldname": "student_group",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Group",
"length": 0,
"no_copy": 0,
"options": "Student Group",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.based_on ==\"Course Schedule\"",
"fieldname": "course_schedule",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Course Schedule",
"length": 0,
"no_copy": 0,
"options": "Course Schedule",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.based_on ==\"Student Group\"",
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
"fieldname": "attendance",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attendance",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Attendance"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "students_html",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Students HTML",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Students HTML"
},
{
"fetch_from": "student_group.academic_year",
"fieldname": "academic_year",
"fieldtype": "Link",
"label": "Academic Year",
"options": "Academic Year",
"read_only": 1
},
{
"fetch_from": "student_group.academic_term",
"fieldname": "academic_term",
"fieldtype": "Link",
"label": "Academic Term",
"options": "Academic Term",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 1,
"hide_toolbar": 1,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-11-10 18:55:36.168044",
"links": [],
"modified": "2020-10-23 17:52:28.078971",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Attendance Tool",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Instructor",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Academics User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
"sort_order": "DESC"
}

View File

@ -43,7 +43,8 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Dosage",
"options": "Prescription Dosage"
"options": "Prescription Dosage",
"reqd": 1
},
{
"fieldname": "period",
@ -51,14 +52,16 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Period",
"options": "Prescription Duration"
"options": "Prescription Duration",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Dosage Form",
"options": "Dosage Form"
"options": "Dosage Form",
"reqd": 1
},
{
"fieldname": "column_break_7",
@ -72,7 +75,7 @@
"label": "Comment"
},
{
"depends_on": "use_interval",
"depends_on": "usage_interval",
"fieldname": "interval",
"fieldtype": "Int",
"in_list_view": 1,
@ -80,6 +83,7 @@
},
{
"default": "1",
"depends_on": "usage_interval",
"fieldname": "update_schedule",
"fieldtype": "Check",
"hidden": 1,
@ -99,12 +103,13 @@
"default": "0",
"fieldname": "usage_interval",
"fieldtype": "Check",
"hidden": 1,
"label": "Dosage by Time Interval"
}
],
"istable": 1,
"links": [],
"modified": "2020-02-26 17:02:42.741338",
"modified": "2020-09-30 23:32:09.495288",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Drug Prescription",

View File

@ -0,0 +1,37 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.set_query('item_code', () => {
return {
filters: {
is_stock_item: 1
}
};
});
frm.set_query('drug_code', 'medication_orders', () => {
return {
filters: {
is_stock_item: 1
}
};
});
},
get_medication_orders: function(frm) {
frappe.call({
method: 'get_medication_orders',
doc: frm.doc,
freeze: true,
freeze_message: __('Fetching Pending Medication Orders'),
callback: function() {
refresh_field('medication_orders');
}
});
}
});

View File

@ -0,0 +1,203 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2020-09-25 14:13:20.111906",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"column_break_3",
"posting_date",
"status",
"filters_section",
"item_code",
"assigned_to_practitioner",
"patient",
"practitioner",
"service_unit",
"column_break_11",
"from_date",
"to_date",
"from_time",
"to_time",
"select_medication_orders_section",
"get_medication_orders",
"medication_orders",
"section_break_18",
"update_stock",
"warehouse",
"amended_from"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HLC-IME-.YYYY.-"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code (Drug)",
"options": "Item"
},
{
"depends_on": "update_stock",
"description": "Warehouse from where medication stock should be consumed",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Medication Warehouse",
"mandatory_depends_on": "update_stock",
"options": "Warehouse"
},
{
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient"
},
{
"fieldname": "service_unit",
"fieldtype": "Link",
"label": "Healthcare Service Unit",
"options": "Healthcare Service Unit"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Inpatient Medication Entry",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "practitioner",
"fieldtype": "Link",
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner"
},
{
"fieldname": "select_medication_orders_section",
"fieldtype": "Section Break",
"label": "Medication Orders"
},
{
"fieldname": "medication_orders",
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "eval:doc.docstatus!==1",
"fieldname": "get_medication_orders",
"fieldtype": "Button",
"label": "Get Pending Medication Orders",
"print_hide": 1
},
{
"fieldname": "assigned_to_practitioner",
"fieldtype": "Link",
"label": "Assigned To",
"options": "User"
},
{
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Stock Details"
},
{
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock"
},
{
"fieldname": "from_time",
"fieldtype": "Time",
"label": "From Time"
},
{
"fieldname": "to_time",
"fieldtype": "Time",
"label": "To Time"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-30 23:40:45.528715",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_link_to_form, getdate, nowtime
from erpnext.stock.utils import get_latest_stock_qty
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class InpatientMedicationEntry(Document):
def validate(self):
self.validate_medication_orders()
def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
if orders:
self.add_mo_to_table(orders)
return self
else:
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])
for data in orders:
self.append('medication_orders', {
'patient': data.patient,
'patient_name': data.patient_name,
'inpatient_record': data.inpatient_record,
'service_unit': data.service_unit,
'datetime': "%s %s" % (data.date, data.time or "00:00:00"),
'drug_code': data.drug,
'drug_name': data.drug_name,
'dosage': data.dosage,
'dosage_form': data.dosage_form,
'against_imo': data.parent,
'against_imoe': data.name
})
def on_submit(self):
self.validate_medication_orders()
success_msg = ""
if self.update_stock:
stock_entry = self.process_stock()
success_msg += _('Stock Entry {0} created and ').format(
frappe.bold(get_link_to_form('Stock Entry', stock_entry)))
self.update_medication_orders()
success_msg += _('Inpatient Medication Orders updated successfully')
frappe.msgprint(success_msg, title=_('Success'), indicator='green')
def validate_medication_orders(self):
for entry in self.medication_orders:
docstatus, is_completed = frappe.db.get_value('Inpatient Medication Order Entry', entry.against_imoe,
['docstatus', 'is_completed'])
if docstatus == 2:
frappe.throw(_('Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}').format(
entry.idx, get_link_to_form(entry.against_imo)))
if is_completed:
frappe.throw(_('Row {0}: This Medication Order is already marked as completed').format(
entry.idx))
def on_cancel(self):
self.cancel_stock_entries()
self.update_medication_orders(on_cancel=True)
def process_stock(self):
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
if not allow_negative_stock:
self.check_stock_qty()
return self.make_stock_entry()
def update_medication_orders(self, on_cancel=False):
orders, order_entry_map = self.get_order_entry_map()
# mark completion status
is_completed = 1
if on_cancel:
is_completed = 0
frappe.db.sql("""
UPDATE `tabInpatient Medication Order Entry`
SET is_completed = %(is_completed)s
WHERE name IN %(orders)s
""", {'orders': orders, 'is_completed': is_completed})
# update status and completed orders count
for order, count in order_entry_map.items():
medication_order = frappe.get_doc('Inpatient Medication Order', order)
completed_orders = flt(count)
current_value = frappe.db.get_value('Inpatient Medication Order', order, 'completed_orders')
if on_cancel:
completed_orders = flt(current_value) - flt(count)
else:
completed_orders = flt(current_value) + flt(count)
medication_order.db_set('completed_orders', completed_orders)
medication_order.set_status()
def get_order_entry_map(self):
# for marking order completion status
orders = []
# orders mapped
order_entry_map = dict()
for entry in self.medication_orders:
orders.append(entry.against_imoe)
parent = entry.against_imo
if not order_entry_map.get(parent):
order_entry_map[parent] = 0
order_entry_map[parent] += 1
return orders, order_entry_map
def check_stock_qty(self):
from erpnext.stock.stock_ledger import NegativeStockError
drug_availability = dict()
for d in self.medication_orders:
if not drug_availability.get(d.drug_code):
drug_availability[d.drug_code] = 0
drug_availability[d.drug_code] += flt(d.dosage)
for drug, dosage in drug_availability.items():
available_qty = get_latest_stock_qty(drug, self.warehouse)
# validate qty
if flt(available_qty) < flt(dosage):
frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
frappe.bold(drug), frappe.bold(self.warehouse))
+ '<br><br>' + _('Available quantity is {0}, you need {1}').format(
frappe.bold(available_qty), frappe.bold(dosage))
+ '<br><br>' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
NegativeStockError, title=_('Insufficient Stock'))
def make_stock_entry(self):
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.purpose = 'Material Issue'
stock_entry.set_stock_entry_type()
stock_entry.from_warehouse = self.warehouse
stock_entry.company = self.company
stock_entry.inpatient_medication_entry = self.name
cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for entry in self.medication_orders:
se_child = stock_entry.append('items')
se_child.item_code = entry.drug_code
se_child.item_name = entry.drug_name
se_child.uom = frappe.db.get_value('Item', entry.drug_code, 'stock_uom')
se_child.stock_uom = se_child.uom
se_child.qty = flt(entry.dosage)
# in stock uom
se_child.conversion_factor = 1
se_child.cost_center = cost_center
se_child.expense_account = expense_account
# references
se_child.patient = entry.patient
se_child.inpatient_medication_entry_child = entry.name
stock_entry.submit()
return stock_entry.name
def cancel_stock_entries(self):
stock_entries = frappe.get_all('Stock Entry', {'inpatient_medication_entry': self.name})
for entry in stock_entries:
doc = frappe.get_doc('Stock Entry', entry.name)
doc.cancel()
def get_pending_medication_orders(entry):
filters, values = get_filters(entry)
data = frappe.db.sql("""
SELECT
ip.inpatient_record, ip.patient, ip.patient_name,
entry.name, entry.parent, entry.drug, entry.drug_name,
entry.dosage, entry.dosage_form, entry.date, entry.time, entry.instructions
FROM
`tabInpatient Medication Order` ip
INNER JOIN
`tabInpatient Medication Order Entry` entry
ON
ip.name = entry.parent
WHERE
ip.docstatus = 1 and
ip.company = %(company)s and
entry.is_completed = 0
{0}
ORDER BY
entry.date, entry.time
""".format(filters), values, as_dict=1)
for doc in data:
inpatient_record = doc.inpatient_record
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if entry.service_unit and doc.service_unit != entry.service_unit:
data.remove(doc)
return data
def get_filters(entry):
filters = ''
values = dict(company=entry.company)
if entry.from_date:
filters += ' and entry.date >= %(from_date)s'
values['from_date'] = entry.from_date
if entry.to_date:
filters += ' and entry.date <= %(to_date)s'
values['to_date'] = entry.to_date
if entry.from_time:
filters += ' and entry.time >= %(from_time)s'
values['from_time'] = entry.from_time
if entry.to_time:
filters += ' and entry.time <= %(to_time)s'
values['to_time'] = entry.to_time
if entry.patient:
filters += ' and ip.patient = %(patient)s'
values['patient'] = entry.patient
if entry.practitioner:
filters += ' and ip.practitioner = %(practitioner)s'
values['practitioner'] = entry.practitioner
if entry.item_code:
filters += ' and entry.drug = %(item_code)s'
values['item_code'] = entry.item_code
if entry.assigned_to_practitioner:
filters += ' and ip._assign LIKE %(assigned_to)s'
values['assigned_to'] = '%' + entry.assigned_to_practitioner + '%'
return filters, values
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
return ip_record.inpatient_occupancies[-1].service_unit

View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'against_imoe',
'internal_links': {
'Inpatient Medication Order': ['medication_orders', 'against_imo']
},
'transactions': [
{
'label': _('Reference'),
'items': ['Inpatient Medication Order']
}
]
}

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import add_days, getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class TestInpatientMedicationEntry(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
frappe.db.sql("""delete from `tabInpatient Medication Order`""")
frappe.db.sql("""delete from `tabInpatient Medication Entry`""")
self.patient = create_patient()
# Admit
ip_record = create_inpatient(self.patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
self.ip_record = ip_record
def test_filters_for_fetching_pending_mo(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
date = add_days(getdate(), -1)
filters = frappe._dict(
from_date=date,
to_date=date,
from_time='',
to_time='',
item_code='Dextromethorphan',
patient=self.patient
)
ipme = create_ipme(filters, update_stock=0)
# 3 dosages per day
self.assertEqual(len(ipme.medication_orders), 3)
self.assertEqual(getdate(ipme.medication_orders[0].datetime), date)
def test_ipme_with_stock_update(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
date = add_days(getdate(), -1)
filters = frappe._dict(
from_date=date,
to_date=date,
from_time='',
to_time='',
item_code='Dextromethorphan',
patient=self.patient
)
make_stock_entry()
ipme = create_ipme(filters, update_stock=1)
ipme.submit()
ipme.reload()
# test order completed
is_order_completed = frappe.db.get_value('Inpatient Medication Order Entry',
ipme.medication_orders[0].against_imoe, 'is_completed')
self.assertEqual(is_order_completed, 1)
# test stock entry
stock_entry = frappe.db.exists('Stock Entry', {'inpatient_medication_entry': ipme.name})
self.assertTrue(stock_entry)
# check references
stock_entry = frappe.get_doc('Stock Entry', stock_entry)
self.assertEqual(stock_entry.items[0].patient, self.patient)
self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
def tearDown(self):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def make_stock_entry():
frappe.db.set_value('Company', '_Test Company', {
'stock_adjustment_account': 'Stock Adjustment - _TC',
'default_inventory_account': 'Stock In Hand - _TC'
})
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.company = '_Test Company'
stock_entry.to_warehouse = 'Stores - _TC'
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
se_child = stock_entry.append('items')
se_child.item_code = 'Dextromethorphan'
se_child.item_name = 'Dextromethorphan'
se_child.uom = 'Nos'
se_child.stock_uom = 'Nos'
se_child.qty = 6
se_child.t_warehouse = 'Stores - _TC'
# in stock uom
se_child.conversion_factor = 1.0
se_child.expense_account = expense_account
stock_entry.submit()

View File

@ -0,0 +1,163 @@
{
"actions": [],
"creation": "2020-09-25 14:56:32.636569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"patient",
"patient_name",
"inpatient_record",
"column_break_4",
"service_unit",
"datetime",
"medication_details_section",
"drug_code",
"drug_name",
"dosage",
"available_qty",
"dosage_form",
"column_break_10",
"instructions",
"references_section",
"against_imo",
"against_imoe"
],
"fields": [
{
"columns": 2,
"fieldname": "patient",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Patient",
"options": "Patient",
"reqd": 1
},
{
"fetch_from": "patient.patient_name",
"fieldname": "patient_name",
"fieldtype": "Data",
"label": "Patient Name",
"read_only": 1
},
{
"columns": 2,
"fieldname": "drug_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Drug Code",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "drug_code.item_name",
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"read_only": 1
},
{
"columns": 1,
"fieldname": "dosage",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Dosage",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"label": "Dosage Form",
"options": "Dosage Form"
},
{
"fetch_from": "patient.inpatient_record",
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"options": "Inpatient Record",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "references_section",
"fieldtype": "Section Break",
"label": "References"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "medication_details_section",
"fieldtype": "Section Break",
"label": "Medication Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"columns": 3,
"fieldname": "datetime",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Datetime",
"reqd": 1
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"label": "Instructions"
},
{
"columns": 2,
"fieldname": "service_unit",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Service Unit",
"options": "Healthcare Service Unit",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "against_imo",
"fieldtype": "Link",
"label": "Against Inpatient Medication Order",
"no_copy": 1,
"options": "Inpatient Medication Order",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "against_imoe",
"fieldtype": "Data",
"label": "Against Inpatient Medication Order Entry",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "available_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Available Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-30 14:48:23.648223",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -0,0 +1,106 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Inpatient Medication Order', {
refresh: function(frm) {
if (frm.doc.docstatus === 1) {
frm.trigger("show_progress");
}
frm.events.show_medication_order_button(frm);
frm.set_query('patient', () => {
return {
filters: {
'inpatient_record': ['!=', '']
}
};
});
},
show_medication_order_button: function(frm) {
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.fields_dict['medication_orders'].grid.add_custom_button(__('Add Medication Orders'), () => {
let d = new frappe.ui.Dialog({
title: __('Add Medication Orders'),
fields: [
{
fieldname: 'drug_code',
label: __('Drug'),
fieldtype: 'Link',
options: 'Item',
reqd: 1,
"get_query": function () {
return {
filters: {'is_stock_item': 1}
};
}
},
{
fieldname: 'dosage',
label: __('Dosage'),
fieldtype: 'Link',
options: 'Prescription Dosage',
reqd: 1
},
{
fieldname: 'period',
label: __('Period'),
fieldtype: 'Link',
options: 'Prescription Duration',
reqd: 1
},
{
fieldname: 'dosage_form',
label: __('Dosage Form'),
fieldtype: 'Link',
options: 'Dosage Form',
reqd: 1
}
],
primary_action_label: __('Add'),
primary_action: () => {
let values = d.get_values();
if (values) {
frm.call({
doc: frm.doc,
method: 'add_order_entries',
args: {
order: values
},
freeze: true,
freeze_message: __('Adding Order Entries'),
callback: function() {
frm.refresh_field('medication_orders');
}
});
}
},
});
d.show();
});
},
show_progress: function(frm) {
let bars = [];
let message = '';
// completed sessions
let title = __('{0} medication orders completed', [frm.doc.completed_orders]);
if (frm.doc.completed_orders === 1) {
title = __('{0} medication order completed', [frm.doc.completed_orders]);
}
title += __(' out of {0}', [frm.doc.total_orders]);
bars.push({
'title': title,
'width': (frm.doc.completed_orders / frm.doc.total_orders * 100) + '%',
'progress_class': 'progress-bar-success'
});
if (bars[0].width == '0%') {
bars[0].width = '0.5%';
}
message = title;
frm.dashboard.add_progress(__('Status'), bars, message);
}
});

View File

@ -0,0 +1,196 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2020-09-14 18:33:56.715736",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"patient_details_section",
"naming_series",
"patient_encounter",
"patient",
"patient_name",
"patient_age",
"inpatient_record",
"column_break_6",
"company",
"status",
"practitioner",
"start_date",
"end_date",
"medication_orders_section",
"medication_orders",
"section_break_16",
"total_orders",
"column_break_18",
"completed_orders",
"amended_from"
],
"fields": [
{
"fieldname": "patient_details_section",
"fieldtype": "Section Break",
"label": "Patient Details"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HLC-IMO-.YYYY.-"
},
{
"fieldname": "patient_encounter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Patient Encounter",
"options": "Patient Encounter"
},
{
"fetch_from": "patient_encounter.patient",
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient",
"read_only_depends_on": "patient_encounter",
"reqd": 1
},
{
"fetch_from": "patient.patient_name",
"fieldname": "patient_name",
"fieldtype": "Data",
"label": "Patient Name",
"read_only": 1
},
{
"fieldname": "patient_age",
"fieldtype": "Data",
"label": "Patient Age",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "patient.inpatient_record",
"fieldname": "inpatient_record",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Inpatient Record",
"options": "Inpatient Record",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "patient_encounter.practitioner",
"fieldname": "practitioner",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
"read_only_depends_on": "patient_encounter"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"read_only": 1
},
{
"depends_on": "eval: doc.patient && doc.start_date",
"fieldname": "medication_orders_section",
"fieldtype": "Section Break",
"label": "Medication Orders"
},
{
"fieldname": "medication_orders",
"fieldtype": "Table",
"label": "Medication Orders",
"options": "Inpatient Medication Order Entry"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Inpatient Medication Order",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Other Details"
},
{
"fieldname": "total_orders",
"fieldtype": "Float",
"label": "Total Orders",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "completed_orders",
"fieldtype": "Float",
"label": "Completed Orders",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-30 21:53:27.128591",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Order",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "patient_encounter, patient",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "patient",
"track_changes": 1
}

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr
from erpnext.healthcare.doctype.patient_encounter.patient_encounter import get_prescription_dates
class InpatientMedicationOrder(Document):
def validate(self):
self.validate_inpatient()
self.validate_duplicate()
self.set_total_orders()
self.set_status()
def on_submit(self):
self.validate_inpatient()
self.set_status()
def on_cancel(self):
self.set_status()
def validate_inpatient(self):
if not self.inpatient_record:
frappe.throw(_('No Inpatient Record found against patient {0}').format(self.patient))
def validate_duplicate(self):
existing_mo = frappe.db.exists('Inpatient Medication Order', {
'patient_encounter': self.patient_encounter,
'docstatus': ('!=', 2),
'name': ('!=', self.name)
})
if existing_mo:
frappe.throw(_('An Inpatient Medication Order {0} against Patient Encounter {1} already exists.').format(
existing_mo, self.patient_encounter), frappe.DuplicateEntryError)
def set_total_orders(self):
self.db_set('total_orders', len(self.medication_orders))
def set_status(self):
status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[cstr(self.docstatus or 0)]
if self.docstatus == 1:
if not self.completed_orders:
status = 'Pending'
elif self.completed_orders < self.total_orders:
status = 'In Process'
else:
status = 'Completed'
self.db_set('status', status)
def add_order_entries(self, order):
if order.get('drug_code'):
dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))
dates = get_prescription_dates(order.get('period'), self.start_date)
for date in dates:
for dose in dosage.dosage_strength:
entry = self.append('medication_orders')
entry.drug = order.get('drug_code')
entry.drug_name = frappe.db.get_value('Item', order.get('drug_code'), 'item_name')
entry.dosage = dose.strength
entry.dosage_form = order.get('dosage_form')
entry.date = date
entry.time = dose.strength_time
self.end_date = dates[-1]
return

View File

@ -0,0 +1,16 @@
frappe.listview_settings['Inpatient Medication Order'] = {
add_fields: ["status"],
filters: [["status", "!=", "Cancelled"]],
get_indicator: function(doc) {
if (doc.status === "Pending") {
return [__("Pending"), "orange", "status,=,Pending"];
} else if (doc.status === "In Process") {
return [__("In Process"), "blue", "status,=,In Process"];
} else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"];
}
}
};

View File

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import add_days, getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
class TestInpatientMedicationOrder(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
self.patient = create_patient()
# Admit
ip_record = create_inpatient(self.patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
self.ip_record = ip_record
def test_order_creation(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
# 3 dosages per day for 2 days
self.assertEqual(len(ipmo.medication_orders), 6)
self.assertEqual(ipmo.medication_orders[0].date, add_days(getdate(), -1))
prescription_dosage = frappe.get_doc('Prescription Dosage', '1-1-1')
for i in range(len(prescription_dosage.dosage_strength)):
self.assertEqual(ipmo.medication_orders[i].time, prescription_dosage.dosage_strength[i].strength_time)
self.assertEqual(ipmo.medication_orders[3].date, getdate())
def test_inpatient_validation(self):
# Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
ipmo = create_ipmo(self.patient)
# inpatient validation
self.assertRaises(frappe.ValidationError, ipmo.insert)
def test_status(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'Pending')
filters = frappe._dict(from_date=add_days(getdate(), -1), to_date=add_days(getdate(), -1), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'In Process')
filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'Completed')
def tearDown(self):
if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def create_dosage_form():
if not frappe.db.exists('Dosage Form', 'Tablet'):
frappe.get_doc({
'doctype': 'Dosage Form',
'dosage_form': 'Tablet'
}).insert()
def create_drug(item=None):
if not item:
item = 'Dextromethorphan'
drug = frappe.db.exists('Item', {'item_code': 'Dextromethorphan'})
if not drug:
drug = frappe.get_doc({
'doctype': 'Item',
'item_code': 'Dextromethorphan',
'item_name': 'Dextromethorphan',
'item_group': 'Products',
'stock_uom': 'Nos',
'is_stock_item': 1,
'valuation_rate': 50,
'opening_stock': 20
}).insert()
def get_orders():
create_dosage_form()
create_drug()
return {
'drug_code': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': '1-1-1',
'dosage_form': 'Tablet',
'period': '2 Day'
}
def create_ipmo(patient):
orders = get_orders()
ipmo = frappe.new_doc('Inpatient Medication Order')
ipmo.patient = patient
ipmo.company = '_Test Company'
ipmo.start_date = add_days(getdate(), -1)
ipmo.add_order_entries(orders)
return ipmo
def create_ipme(filters, update_stock=0):
ipme = frappe.new_doc('Inpatient Medication Entry')
ipme.company = '_Test Company'
ipme.posting_date = getdate()
ipme.update_stock = update_stock
if update_stock:
ipme.warehouse = 'Stores - _TC'
for key, value in filters.items():
ipme.set(key, value)
ipme = ipme.get_medication_orders()
return ipme

View File

@ -0,0 +1,94 @@
{
"actions": [],
"creation": "2020-09-14 21:51:30.259164",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"drug",
"drug_name",
"dosage",
"dosage_form",
"instructions",
"column_break_4",
"date",
"time",
"is_completed"
],
"fields": [
{
"fieldname": "drug",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Drug",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "drug.item_name",
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"read_only": 1
},
{
"fieldname": "dosage",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Dosage",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Dosage Form",
"options": "Dosage Form",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Time",
"reqd": 1
},
{
"default": "0",
"fieldname": "is_completed",
"fieldtype": "Check",
"label": "Is Order Completed",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"label": "Instructions"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-30 14:03:26.755925",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Order Entry",
"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 InpatientMedicationOrderEntry(Document):
pass

View File

@ -83,6 +83,7 @@ def get_healthcare_service_unit():
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.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
service_unit.occupancy_status = "Vacant"

View File

@ -58,6 +58,14 @@ frappe.ui.form.on('Patient Encounter', {
create_procedure(frm);
},'Create');
if (frm.doc.drug_prescription && frm.doc.inpatient_record && frm.doc.inpatient_status === "Admitted") {
frm.add_custom_button(__('Inpatient Medication Order'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.patient_encounter.patient_encounter.make_ip_medication_order',
frm: frm
});
}, 'Create');
}
}
frm.set_query('patient', function() {

View File

@ -6,8 +6,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr
from frappe.utils import cstr, getdate, add_days
from frappe import _
from frappe.model.mapper import get_mapped_doc
class PatientEncounter(Document):
def validate(self):
@ -22,20 +23,69 @@ class PatientEncounter(Document):
insert_encounter_to_medical_record(self)
def on_submit(self):
update_encounter_medical_record(self)
if self.therapies:
create_therapy_plan(self)
def on_cancel(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
delete_medical_record(self)
def on_submit(self):
create_therapy_plan(self)
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]
@frappe.whitelist()
def make_ip_medication_order(source_name, target_doc=None):
def set_missing_values(source, target):
target.start_date = source.encounter_date
for entry in source.drug_prescription:
if entry.drug_code:
dosage = frappe.get_doc('Prescription Dosage', entry.dosage)
dates = get_prescription_dates(entry.period, target.start_date)
for date in dates:
for dose in dosage.dosage_strength:
order = target.append('medication_orders')
order.drug = entry.drug_code
order.drug_name = entry.drug_name
order.dosage = dose.strength
order.instructions = entry.comment
order.dosage_form = entry.dosage_form
order.date = date
order.time = dose.strength_time
target.end_date = dates[-1]
doc = get_mapped_doc('Patient Encounter', source_name, {
'Patient Encounter': {
'doctype': 'Inpatient Medication Order',
'field_map': {
'name': 'patient_encounter',
'patient': 'patient',
'patient_name': 'patient_name',
'patient_age': 'patient_age',
'inpatient_record': 'inpatient_record',
'practitioner': 'practitioner',
'start_date': 'encounter_date'
},
}
}, target_doc, set_missing_values)
return doc
def get_prescription_dates(period, start_date):
prescription_duration = frappe.get_doc('Prescription Duration', period)
days = prescription_duration.get_days()
dates = [start_date]
for i in range(1, days):
dates.append(add_days(getdate(start_date), i))
return dates
def create_therapy_plan(encounter):
if len(encounter.therapies):
doc = frappe.new_doc('Therapy Plan')
@ -51,6 +101,7 @@ def create_therapy_plan(encounter):
encounter.db_set('therapy_plan', doc.name)
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')
@ -63,6 +114,7 @@ def insert_encounter_to_medical_record(doc):
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})
@ -72,8 +124,17 @@ def update_encounter_medical_record(encounter):
else:
insert_encounter_to_medical_record(encounter)
def delete_medical_record(encounter):
frappe.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name)
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>'

View File

@ -5,12 +5,18 @@ def get_data():
return {
'fieldname': 'encounter',
'non_standard_fieldnames': {
'Patient Medical Record': 'reference_name'
'Patient Medical Record': 'reference_name',
'Inpatient Medication Order': 'patient_encounter'
},
'transactions': [
{
'label': _('Records'),
'items': ['Vital Signs', 'Patient Medical Record']
},
]
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
}
],
'disable_create_buttons': ['Inpatient Medication Order']
}

View File

@ -5,9 +5,9 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate
from frappe.utils import getdate, flt
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
class TestTherapyPlan(unittest.TestCase):
@ -20,25 +20,45 @@ class TestTherapyPlan(unittest.TestCase):
plan = create_therapy_plan()
self.assertEquals(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
def test_therapy_plan_from_template(self):
patient = create_patient()
template = create_therapy_plan_template()
# check linked item
self.assertTrue(frappe.db.exists('Therapy Plan Template', {'linked_item': 'Complete Rehab'}))
def create_therapy_plan():
plan = create_therapy_plan(template)
# invoice
si = make_sales_invoice(plan.name, patient, '_Test Company', template)
si.save()
therapy_plan_template_amt = frappe.db.get_value('Therapy Plan Template', template, 'total_amount')
self.assertEquals(si.items[0].amount, therapy_plan_template_amt)
def create_therapy_plan(template=None):
patient = create_patient()
therapy_type = create_therapy_type()
plan = frappe.new_doc('Therapy Plan')
plan.patient = patient
plan.start_date = getdate()
if template:
plan.therapy_plan_template = template
plan = plan.set_therapy_details_from_template()
else:
plan.append('therapy_plan_details', {
'therapy_type': therapy_type.name,
'no_of_sessions': 2
})
plan.save()
return plan
@ -55,3 +75,22 @@ def create_encounter(patient, medical_department, practitioner):
encounter.save()
encounter.submit()
return encounter
def create_therapy_plan_template():
template_name = frappe.db.exists('Therapy Plan Template', 'Complete Rehab')
if not template_name:
therapy_type = create_therapy_type()
template = frappe.new_doc('Therapy Plan Template')
template.plan_name = template.item_code = template.item_name = 'Complete Rehab'
template.item_group = 'Services'
rate = frappe.db.get_value('Therapy Type', therapy_type.name, 'rate')
template.append('therapy_types', {
'therapy_type': therapy_type.name,
'no_of_sessions': 2,
'rate': rate,
'amount': 2 * flt(rate)
})
template.save()
template_name = template.name
return template_name

View File

@ -37,7 +37,8 @@ frappe.ui.form.on('Therapy Plan', {
args: {
therapy_plan: frm.doc.name,
patient: frm.doc.patient,
therapy_type: data.therapy_type
therapy_type: data.therapy_type,
company: frm.doc.company
},
freeze: true,
callback: function(r) {
@ -49,13 +50,53 @@ frappe.ui.form.on('Therapy Plan', {
});
}, __('Select Therapy Type'), __('Create'));
}, __('Create'));
if (frm.doc.therapy_plan_template && !frm.doc.invoiced) {
frm.add_custom_button(__('Sales Invoice'), function() {
frm.trigger('make_sales_invoice');
}, __('Create'));
}
}
if (frm.doc.therapy_plan_template) {
frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1;
frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1;
}
},
make_sales_invoice: function(frm) {
frappe.call({
args: {
'reference_name': frm.doc.name,
'patient': frm.doc.patient,
'company': frm.doc.company,
'therapy_plan_template': frm.doc.therapy_plan_template
},
method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_sales_invoice',
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
}
});
},
therapy_plan_template: function(frm) {
if (frm.doc.therapy_plan_template) {
frappe.call({
method: 'set_therapy_details_from_template',
doc: frm.doc,
freeze: true,
freeze_message: __('Fetching Template Details'),
callback: function() {
refresh_field('therapy_plan_details');
}
});
}
},
show_progress_for_therapies: function(frm) {
let bars = [];
let message = '';
let added_min = false;
// completed sessions
let title = __('{0} sessions completed', [frm.doc.total_sessions_completed]);
@ -71,7 +112,6 @@ frappe.ui.form.on('Therapy Plan', {
});
if (bars[0].width == '0%') {
bars[0].width = '0.5%';
added_min = 0.5;
}
message = title;
frm.dashboard.add_progress(__('Status'), bars, message);

View File

@ -9,11 +9,13 @@
"naming_series",
"patient",
"patient_name",
"invoiced",
"column_break_4",
"company",
"status",
"start_date",
"section_break_3",
"therapy_plan_template",
"therapy_plan_details",
"title",
"section_break_9",
@ -46,6 +48,7 @@
"fieldtype": "Table",
"label": "Therapy Plan Details",
"options": "Therapy Plan Detail",
"read_only_depends_on": "therapy_plan_template",
"reqd": 1
},
{
@ -105,11 +108,27 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company"
"options": "Company",
"reqd": 1
},
{
"fieldname": "therapy_plan_template",
"fieldtype": "Link",
"label": "Therapy Plan Template",
"options": "Therapy Plan Template"
},
{
"default": "0",
"fieldname": "invoiced",
"fieldtype": "Check",
"label": "Invoiced",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"links": [],
"modified": "2020-05-25 14:38:53.649315",
"modified": "2020-10-23 01:27:42.128855",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import today
from frappe.utils import flt, today
class TherapyPlan(Document):
def validate(self):
@ -33,13 +33,26 @@ class TherapyPlan(Document):
self.db_set('total_sessions', total_sessions)
self.db_set('total_sessions_completed', total_sessions_completed)
def set_therapy_details_from_template(self):
# Add therapy types in the child table
self.set('therapy_plan_details', [])
therapy_plan_template = frappe.get_doc('Therapy Plan Template', self.therapy_plan_template)
for data in therapy_plan_template.therapy_types:
self.append('therapy_plan_details', {
'therapy_type': data.therapy_type,
'no_of_sessions': data.no_of_sessions
})
return self
@frappe.whitelist()
def make_therapy_session(therapy_plan, patient, therapy_type):
def make_therapy_session(therapy_plan, patient, therapy_type, company):
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session')
therapy_session.therapy_plan = therapy_plan
therapy_session.company = company
therapy_session.patient = patient
therapy_session.therapy_type = therapy_type.name
therapy_session.duration = therapy_type.default_duration
@ -49,3 +62,38 @@ def make_therapy_session(therapy_plan, patient, therapy_type):
if frappe.flags.in_test:
therapy_session.start_date = today()
return therapy_session.as_dict()
@frappe.whitelist()
def make_sales_invoice(reference_name, patient, company, therapy_plan_template):
from erpnext.stock.get_item_details import get_item_details
si = frappe.new_doc('Sales Invoice')
si.company = company
si.patient = patient
si.customer = frappe.db.get_value('Patient', patient, 'customer')
item = frappe.db.get_value('Therapy Plan Template', therapy_plan_template, 'linked_item')
price_list, price_list_currency = frappe.db.get_values('Price List', {'selling': 1}, ['name', 'currency'])[0]
args = {
'doctype': 'Sales Invoice',
'item_code': item,
'company': company,
'customer': si.customer,
'selling_price_list': price_list,
'price_list_currency': price_list_currency,
'plc_conversion_rate': 1.0,
'conversion_rate': 1.0
}
item_line = si.append('items', {})
item_details = get_item_details(args)
item_line.item_code = item
item_line.qty = 1
item_line.rate = item_details.price_list_rate
item_line.amount = flt(item_line.rate) * flt(item_line.qty)
item_line.reference_dt = 'Therapy Plan'
item_line.reference_dn = reference_name
item_line.description = item_details.description
si.set_missing_values(for_validate = True)
return si

View File

@ -4,10 +4,18 @@ from frappe import _
def get_data():
return {
'fieldname': 'therapy_plan',
'non_standard_fieldnames': {
'Sales Invoice': 'reference_dn'
},
'transactions': [
{
'label': _('Therapy Sessions'),
'items': ['Therapy Session']
},
{
'label': _('Billing'),
'items': ['Sales Invoice']
}
]
],
'disable_create_buttons': ['Sales Invoice']
}

View File

@ -35,7 +35,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-03-30 22:02:01.740109",
"modified": "2020-10-08 01:17:34.778028",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan Detail",

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestTherapyPlanTemplate(unittest.TestCase):
pass

View File

@ -0,0 +1,57 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Therapy Plan Template', {
refresh: function(frm) {
frm.set_query('therapy_type', 'therapy_types', () => {
return {
filters: {
'is_billable': 1
}
};
});
},
set_totals: function(frm) {
let total_sessions = 0;
let total_amount = 0.0;
frm.doc.therapy_types.forEach((d) => {
if (d.no_of_sessions) total_sessions += cint(d.no_of_sessions);
if (d.amount) total_amount += flt(d.amount);
});
frm.set_value('total_sessions', total_sessions);
frm.set_value('total_amount', total_amount);
frm.refresh_fields();
}
});
frappe.ui.form.on('Therapy Plan Template Detail', {
therapy_type: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frappe.call('frappe.client.get', {
doctype: 'Therapy Type',
name: row.therapy_type
}).then((res) => {
row.rate = res.message.rate;
if (!row.no_of_sessions)
row.no_of_sessions = 1;
row.amount = flt(row.rate) * cint(row.no_of_sessions);
frm.refresh_field('therapy_types');
frm.trigger('set_totals');
});
},
no_of_sessions: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.amount = flt(row.rate) * cint(row.no_of_sessions);
frm.refresh_field('therapy_types');
frm.trigger('set_totals');
},
rate: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.amount = flt(row.rate) * cint(row.no_of_sessions);
frm.refresh_field('therapy_types');
frm.trigger('set_totals');
}
});

View File

@ -0,0 +1,132 @@
{
"actions": [],
"autoname": "field:plan_name",
"creation": "2020-09-22 17:51:38.861055",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"plan_name",
"linked_item_details_section",
"item_code",
"item_name",
"item_group",
"column_break_6",
"description",
"linked_item",
"therapy_types_section",
"therapy_types",
"section_break_11",
"total_sessions",
"column_break_13",
"total_amount"
],
"fields": [
{
"fieldname": "plan_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Plan Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "therapy_types_section",
"fieldtype": "Section Break",
"label": "Therapy Types"
},
{
"fieldname": "therapy_types",
"fieldtype": "Table",
"label": "Therapy Types",
"options": "Therapy Plan Template Detail",
"reqd": 1
},
{
"fieldname": "linked_item",
"fieldtype": "Link",
"label": "Linked Item",
"options": "Item",
"read_only": 1
},
{
"fieldname": "linked_item_details_section",
"fieldtype": "Section Break",
"label": "Linked Item Details"
},
{
"fieldname": "item_code",
"fieldtype": "Data",
"label": "Item Code",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Item Description"
},
{
"fieldname": "total_amount",
"fieldtype": "Currency",
"label": "Total Amount",
"read_only": 1
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"fieldname": "total_sessions",
"fieldtype": "Int",
"label": "Total Sessions",
"read_only": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-08 00:56:58.062105",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan Template",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 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,73 @@
# -*- 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
from frappe.utils import cint, flt
from erpnext.healthcare.doctype.therapy_type.therapy_type import make_item_price
class TherapyPlanTemplate(Document):
def after_insert(self):
self.create_item_from_template()
def validate(self):
self.set_totals()
def on_update(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save: return
if doc_before_save.item_name != self.item_name or doc_before_save.item_group != self.item_group \
or doc_before_save.description != self.description:
self.update_item()
if doc_before_save.therapy_types != self.therapy_types:
self.update_item_price()
def set_totals(self):
total_sessions = 0
total_amount = 0
for entry in self.therapy_types:
total_sessions += cint(entry.no_of_sessions)
total_amount += flt(entry.amount)
self.total_sessions = total_sessions
self.total_amount = total_amount
def create_item_from_template(self):
uom = frappe.db.exists('UOM', 'Nos') or frappe.db.get_single_value('Stock Settings', 'stock_uom')
item = frappe.get_doc({
'doctype': 'Item',
'item_code': self.item_code,
'item_name': self.item_name,
'item_group': self.item_group,
'description': self.description,
'is_sales_item': 1,
'is_service_item': 1,
'is_purchase_item': 0,
'is_stock_item': 0,
'show_in_website': 0,
'is_pro_applicable': 0,
'stock_uom': uom
}).insert(ignore_permissions=True, ignore_mandatory=True)
make_item_price(item.name, self.total_amount)
self.db_set('linked_item', item.name)
def update_item(self):
item_doc = frappe.get_doc('Item', {'item_code': self.linked_item})
item_doc.item_name = self.item_name
item_doc.item_group = self.item_group
item_doc.description = self.description
item_doc.ignore_mandatory = True
item_doc.save(ignore_permissions=True)
def update_item_price(self):
item_price = frappe.get_doc('Item Price', {'item_code': self.linked_item})
item_price.item_name = self.item_name
item_price.price_list_rate = self.total_amount
item_price.ignore_mandatory = True
item_price.save(ignore_permissions=True)

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'therapy_plan_template',
'transactions': [
{
'label': _('Therapy Plans'),
'items': ['Therapy Plan']
}
]
}

View File

@ -0,0 +1,54 @@
{
"actions": [],
"creation": "2020-10-07 23:04:44.373381",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"therapy_type",
"no_of_sessions",
"rate",
"amount"
],
"fields": [
{
"fieldname": "therapy_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Therapy Type",
"options": "Therapy Type",
"reqd": 1
},
{
"fieldname": "no_of_sessions",
"fieldtype": "Int",
"in_list_view": 1,
"label": "No of Sessions"
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-10-07 23:46:54.296322",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -92,7 +92,8 @@ frappe.ui.form.on('Therapy Session', {
'start_date': data.message.appointment_date,
'start_time': data.message.appointment_time,
'service_unit': data.message.service_unit,
'company': data.message.company
'company': data.message.company,
'duration': data.message.duration
};
frm.set_value(values);
}
@ -107,6 +108,7 @@ frappe.ui.form.on('Therapy Session', {
'start_date': '',
'start_time': '',
'service_unit': '',
'duration': ''
};
frm.set_value(values);
}

View File

@ -47,7 +47,8 @@
"fieldname": "appointment",
"fieldtype": "Link",
"label": "Appointment",
"options": "Patient Appointment"
"options": "Patient Appointment",
"set_only_once": 1
},
{
"fieldname": "patient",
@ -90,7 +91,8 @@
"fetch_from": "therapy_template.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration"
"label": "Duration",
"reqd": 1
},
{
"fieldname": "location",
@ -220,7 +222,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-30 10:56:10.354268",
"modified": "2020-10-22 23:10:21.178644",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Session",

View File

@ -4,16 +4,41 @@
from __future__ import unicode_literals
import frappe
import datetime
from frappe.model.document import Document
from frappe.utils import get_time, flt
from frappe.model.mapper import get_mapped_doc
from frappe import _
from frappe.utils import cstr, getdate
from frappe.utils import cstr, getdate, get_link_to_form
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
class TherapySession(Document):
def validate(self):
self.validate_duplicate()
self.set_total_counts()
def validate_duplicate(self):
end_time = datetime.datetime.combine(getdate(self.start_date), get_time(self.start_time)) \
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select
name
from
`tabTherapy Session`
where
start_date=%s and name!=%s and docstatus!=2
and (practitioner=%s or patient=%s) and
((start_time<%s and start_time + INTERVAL duration MINUTE>%s) or
(start_time>%s and start_time<%s) or
(start_time=%s))
""", (self.start_date, self.name, self.practitioner, self.patient,
self.start_time, end_time.time(), self.start_time, end_time.time(), self.start_time))
if overlaps:
overlapping_details = _('Therapy Session overlaps with {0}').format(get_link_to_form('Therapy Session', overlaps[0][0]))
frappe.throw(overlapping_details, title=_('Therapy Sessions Overlapping'))
def on_submit(self):
self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)

View File

@ -41,7 +41,7 @@ class TherapyType(Document):
if self.rate:
item_price = frappe.get_doc('Item Price', {'item_code': self.item})
item_price.item_name = self.item_name
item_price.price_list_name = self.rate
item_price.price_list_rate = self.rate
item_price.ignore_mandatory = True
item_price.save()

View File

@ -23,9 +23,9 @@ def get_healthcare_services_to_invoice(patient, company):
items_to_invoice += get_lab_tests_to_invoice(patient, company)
items_to_invoice += get_clinical_procedures_to_invoice(patient, company)
items_to_invoice += get_inpatient_services_to_invoice(patient, company)
items_to_invoice += get_therapy_plans_to_invoice(patient, company)
items_to_invoice += get_therapy_sessions_to_invoice(patient, company)
return items_to_invoice
@ -35,6 +35,7 @@ def validate_customer_created(patient):
msg += " <b><a href='#Form/Patient/{0}'>{0}</a></b>".format(patient.name)
frappe.throw(msg, title=_('Customer Not Found'))
def get_appointments_to_invoice(patient, company):
appointments_to_invoice = []
patient_appointments = frappe.get_list(
@ -246,12 +247,44 @@ def get_inpatient_services_to_invoice(patient, company):
return services_to_invoice
def get_therapy_plans_to_invoice(patient, company):
therapy_plans_to_invoice = []
therapy_plans = frappe.get_list(
'Therapy Plan',
fields=['therapy_plan_template', 'name'],
filters={
'patient': patient.name,
'invoiced': 0,
'company': company,
'therapy_plan_template': ('!=', '')
}
)
for plan in therapy_plans:
therapy_plans_to_invoice.append({
'reference_type': 'Therapy Plan',
'reference_name': plan.name,
'service': frappe.db.get_value('Therapy Plan Template', plan.therapy_plan_template, 'linked_item')
})
return therapy_plans_to_invoice
def get_therapy_sessions_to_invoice(patient, company):
therapy_sessions_to_invoice = []
therapy_plans = frappe.db.get_all('Therapy Plan', {'therapy_plan_template': ('!=', '')})
therapy_plans_created_from_template = []
for entry in therapy_plans:
therapy_plans_created_from_template.append(entry.name)
therapy_sessions = frappe.get_list(
'Therapy Session',
fields='*',
filters={'patient': patient.name, 'invoiced': 0, 'company': company}
filters={
'patient': patient.name,
'invoiced': 0,
'company': company,
'therapy_plan': ('not in', therapy_plans_created_from_template)
}
)
for therapy in therapy_sessions:
if not therapy.appointment:
@ -368,8 +401,8 @@ def validate_invoiced_on_submit(item):
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
if is_invoiced:
frappe.throw(_('The item referenced by {0} - {1} is already invoiced'\
).format(item.reference_dt, item.reference_dn))
frappe.throw(_('The item referenced by {0} - {1} is already invoiced').format(
item.reference_dt, item.reference_dn))
def manage_prescriptions(invoiced, ref_dt, ref_dn, dt, created_check_field):

View File

@ -282,7 +282,8 @@ doc_events = {
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [
"Payment Entry"
"Payment Entry",
"Inpatient Medication Entry"
]
scheduler_events = {

View File

@ -56,23 +56,35 @@ frappe.ui.form.on('Production Plan', {
refresh: function(frm) {
if (frm.doc.docstatus === 1) {
frm.trigger("show_progress");
}
if (frm.doc.docstatus === 1 && frm.doc.po_items
&& frm.doc.status != 'Completed') {
if (frm.doc.status !== "Completed") {
if (frm.doc.po_items && frm.doc.status !== "Closed") {
frm.add_custom_button(__("Work Order"), ()=> {
frm.trigger("make_work_order");
}, __('Create'));
}
if (frm.doc.docstatus === 1 && frm.doc.mr_items
&& !in_list(['Material Requested', 'Completed'], frm.doc.status)) {
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
}
if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() {
frm.events.close_open_production_plan(frm, false);
}, __("Status"));
} else {
frm.add_custom_button(__("Close"), function() {
frm.events.close_open_production_plan(frm, true);
}, __("Status"));
}
}
}
if (frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
frm.trigger("material_requirement");
const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
@ -121,6 +133,18 @@ frappe.ui.form.on('Production Plan', {
set_field_options("projected_qty_formula", projected_qty_formula);
},
close_open_production_plan: (frm, close=false) => {
frappe.call({
method: "set_status",
freeze: true,
doc: frm.doc,
args: {close : close},
callback: function() {
frm.reload_doc();
}
});
},
make_work_order: function(frm) {
frappe.call({
method: "make_work_order",

View File

@ -275,7 +275,7 @@
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled\nMaterial Requested",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested",
"print_hide": 1,
"read_only": 1
},
@ -304,9 +304,10 @@
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-02-03 00:25:25.934202",
"modified": "2020-10-26 13:00:54.335319",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@ -219,13 +219,17 @@ class ProductionPlan(Document):
filters = {'docstatus': 0, 'production_plan': ("=", self.name)}):
frappe.delete_doc('Work Order', d.name)
def set_status(self):
def set_status(self, close=None):
self.status = {
0: 'Draft',
1: 'Submitted',
2: 'Cancelled'
}.get(self.docstatus)
if close:
self.db_set('status', 'Closed')
return
if self.total_produced_qty > 0:
self.status = "In Process"
if self.total_produced_qty == self.total_planned_qty:
@ -235,6 +239,9 @@ class ProductionPlan(Document):
self.update_ordered_status()
self.update_requested_status()
if close is not None:
self.db_set('status', self.status)
def update_ordered_status(self):
update_status = False
for d in self.po_items:
@ -735,10 +742,12 @@ def get_items_for_material_requests(doc, warehouses=None):
mr_items = new_mr_items
if not mr_items:
frappe.msgprint(_("""As raw materials projected quantity is more than required quantity,
there is no need to create material request for the warehouse {0}.
Still if you want to make material request,
kindly enable <b>Ignore Existing Projected Quantity</b> checkbox""").format(doc.get('for_warehouse')))
to_enable = frappe.bold(_("Ignore Existing Projected Quantity"))
warehouse = frappe.bold(doc.get('for_warehouse'))
message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "<br><br>"
message += _(" If you still want to proceed, please enable {0}.").format(to_enable)
frappe.msgprint(message, title=_("Note"))
return mr_items

View File

@ -1,6 +1,6 @@
frappe.listview_settings['Production Plan'] = {
add_fields: ["status"],
filters: [["status", "!=", "Stopped"]],
filters: [["status", "!=", "Closed"]],
get_indicator: function(doc) {
if(doc.status==="Submitted") {
return [__("Not Started"), "orange", "status,=,Submitted"];
@ -10,7 +10,8 @@ frappe.listview_settings['Production Plan'] = {
"In Process": "orange",
"Completed": "green",
"Material Requested": "darkgrey",
"Cancelled": "darkgrey"
"Cancelled": "darkgrey",
"Closed": "grey"
}[doc.status], "status,=," + doc.status];
}
}

View File

@ -11,30 +11,20 @@
{
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Warehouse",
"options": "Warehouse"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-02-02 10:37:16.650836",
"modified": "2020-10-26 12:55:00.778201",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Material Request Warehouse",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",

View File

@ -731,3 +731,5 @@ erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v13_0.update_reason_for_resignation_in_employee

View File

@ -0,0 +1,10 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.domains.healthcare import data
def execute():
if 'Healthcare' not in frappe.get_active_domains():
return
if data['custom_fields']:
create_custom_fields(data['custom_fields'])

View File

@ -53,7 +53,7 @@ def execute():
# renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity
for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']:
if frappe.db.exists('Report', report):
frappe.delete_doc('Report', report)
frappe.delete_doc('Report', report, ignore_permissions=True)
def convert_to_seconds(value, unit):

View File

@ -0,0 +1,15 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("hr", "doctype", "employee")
if frappe.db.has_column("Employee", "reason_for_resignation"):
frappe.db.sql(""" UPDATE `tabEmployee`
SET reason_for_leaving = reason_for_resignation
WHERE status = 'Left' and reason_for_leaving is null and reason_for_resignation is not null
""")

View File

@ -210,6 +210,7 @@
[data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; }
[data-route="point-of-sale"] .total-summary-wrapper:last-child { border-bottom: none; }
[data-route="point-of-sale"] .invoices-container .invoice-wrapper:last-child { border-bottom: none; }
[data-route="point-of-sale"] .new-btn { background-color: #5e64ff; color: white; border: none;}
[data-route="point-of-sale"] .summary-btns:last-child { margin-right: 0px; }
[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }

View File

@ -6,6 +6,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
apply_pricing_rule_on_item: function(item){
let effective_item_rate = item.price_list_rate;
let item_rate = item.rate;
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
effective_item_rate = item.blanket_order_rate;
}
@ -17,15 +18,17 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}
item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate);
item.rate = flt(item.rate_with_margin , precision("rate", item));
item_rate = flt(item.rate_with_margin , precision("rate", item));
if(item.discount_percentage){
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
if (item.discount_amount) {
item.rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
}
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
},
calculate_taxes_and_totals: function(update_paid_amount) {
@ -88,11 +91,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
if(this.frm.doc.currency == company_currency) {
this.frm.set_value("conversion_rate", 1);
} else {
const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', [
conversion_rate_label,
this.frm.doc.currency,
company_currency
]);
const subs = [conversion_rate_label, this.frm.doc.currency, company_currency];
const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', subs);
frappe.throw(err_message);
}
}

View File

@ -352,9 +352,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let show_description = function(idx, exist = null) {
if (exist) {
scan_barcode_field.set_new_description(__('Row #{0}: Qty increased by 1', [idx]));
frappe.show_alert({
message: __('Row #{0}: Qty increased by 1', [idx]),
indicator: 'green'
});
} else {
scan_barcode_field.set_new_description(__('Row #{0}: Item added', [idx]));
frappe.show_alert({
message: __('Row #{0}: Item added', [idx]),
indicator: 'green'
});
}
}
@ -365,7 +371,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}).then(r => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
scan_barcode_field.set_new_description(__('Cannot find Item with this barcode'));
frappe.show_alert({
message: __('Cannot find Item with this Barcode'),
indicator: 'red'
});
return;
}
@ -651,7 +660,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = rate;
child.rate = 0;
}
});
}
@ -1049,14 +1058,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(item.item_code && item.uom) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_conversion_factor",
child: item,
args: {
item_code: item.item_code,
uom: item.uom
},
callback: function(r) {
if(!r.exc) {
me.conversion_factor(me.frm.doc, cdt, cdn);
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
}
}
});

View File

@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
default: 0
default: flt(me.item.stock_qty),
},
{
fieldname: 'auto_fetch_button',
@ -91,7 +91,8 @@ erpnext.SerialNoBatchSelector = Class.extend({
qty: qty,
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
batch_no: me.item.batch_no || null
batch_no: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
}
});
@ -100,11 +101,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
let records_length = auto_fetched_serial_numbers.length;
if (!records_length) {
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
under warehouse ${warehouse}. Please try changing warehouse.`));
frappe.msgprint(
__('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [me.item.item_code.bold(), warehouse])
);
}
if (records_length < qty) {
frappe.msgprint(__(`Fetched only ${records_length} available serial numbers.`));
frappe.msgprint(__('Fetched only {0} available serial numbers.', [records_length]));
}
let serial_no_list_field = this.dialog.fields_dict.serial_no;
numbers = auto_fetched_serial_numbers.join('\n');
@ -189,15 +191,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
}
if(this.has_batch && !this.has_serial_no) {
if(values.batches.length === 0 || !values.batches) {
frappe.throw(__("Please select batches for batched item "
+ values.item_code));
return false;
frappe.throw(__("Please select batches for batched item {0}", [values.item_code]));
}
values.batches.map((batch, i) => {
if(!batch.selected_qty || batch.selected_qty === 0 ) {
if (!this.show_dialog) {
frappe.throw(__("Please select quantity on row " + (i+1)));
return false;
frappe.throw(__("Please select quantity on row {0}", [i+1]));
}
}
});
@ -206,9 +205,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
} else {
let serial_nos = values.serial_no || '';
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
frappe.throw(__("Please enter serial numbers for serialized item "
+ values.item_code));
return false;
frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code]));
}
return true;
}
@ -355,8 +352,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
});
if (selected_batches.includes(val)) {
this.set_value("");
frappe.throw(__(`Batch ${val} already selected.`));
return;
frappe.throw(__('Batch {0} already selected.', [val]));
}
if (me.warehouse_details.name) {
@ -375,8 +371,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
} else {
this.set_value("");
frappe.throw(__(`Please select a warehouse to get available
quantities`));
frappe.throw(__('Please select a warehouse to get available quantities'));
}
// e.stopImmediatePropagation();
}
@ -411,8 +406,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
parseFloat(available_qty) < parseFloat(selected_qty)) {
this.set_value('0');
frappe.throw(__(`For transfer from source, selected quantity cannot be
greater than available quantity`));
frappe.throw(__('For transfer from source, selected quantity cannot be greater than available quantity'));
} else {
this.grid.refresh();
}
@ -451,20 +445,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
args: {
filters: {
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : ''
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
}
}
}).then((data) => {
if (!data.message[1].length) {
this.showing_reserved_serial_nos_error = true;
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
const d = frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
under warehouse ${warehouse}. Please try changing warehouse.`));
d.get_close_btn().on('click', () => {
this.showing_reserved_serial_nos_error = false;
d.hide();
});
}
serial_no_filters['name'] = ["not in", data.message[0]]
})
}

View File

@ -18,7 +18,7 @@
{
"hidden": 0,
"label": "Review and Action",
"links": "[\n {\n \"description\": \"Quality Review\",\n \"label\": \"Quality Review\",\n \"name\": \"Quality Review\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Action\",\n \"label\": \"Quality Action\",\n \"name\": \"Quality Action\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"Non Conformance\",\n \"label\": \"Non Conformance\",\n \"name\": \"Non Conformance\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Review\",\n \"label\": \"Quality Review\",\n \"name\": \"Quality Review\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Action\",\n \"label\": \"Quality Action\",\n \"name\": \"Quality Action\",\n \"type\": \"doctype\"\n }\n]"
}
],
"category": "Modules",
@ -29,11 +29,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"icon": "",
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Quality",
"modified": "2020-04-01 11:28:51.095012",
"modified": "2020-10-27 16:28:54.138055",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality",
@ -47,6 +47,7 @@
"type": "DocType"
},
{
"doc_view": "Tree",
"label": "Quality Procedure",
"link_to": "Quality Procedure",
"type": "DocType"
@ -55,6 +56,33 @@
"label": "Quality Inspection",
"link_to": "Quality Inspection",
"type": "DocType"
},
{
"color": "#ff8989",
"doc_view": "",
"format": "{} Open",
"label": "Quality Review",
"link_to": "Quality Review",
"stats_filter": "{\"status\": \"Open\"}",
"type": "DocType"
},
{
"color": "#ff8989",
"doc_view": "",
"format": "{} Open",
"label": "Quality Action",
"link_to": "Quality Action",
"stats_filter": "{\"status\": \"Open\"}",
"type": "DocType"
},
{
"color": "#ff8989",
"doc_view": "",
"format": "{} Open",
"label": "Non Conformance",
"link_to": "Non Conformance",
"stats_filter": "{\"status\": \"Open\"}",
"type": "DocType"
}
]
}

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