Merge branch 'develop' into education-module-fixes
This commit is contained in:
commit
5ecf6b34b0
@ -40,7 +40,7 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"description": "If enabled, the system will post accounting entries for inventory automatically.",
|
"description": "If enabled, the system will post accounting entries for inventory automatically",
|
||||||
"fieldname": "auto_accounting_for_stock",
|
"fieldname": "auto_accounting_for_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@ -48,23 +48,23 @@
|
|||||||
"label": "Make Accounting Entry For Every Stock Movement"
|
"label": "Make Accounting Entry For Every Stock Movement"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Accounting entry frozen up to this date, nobody can do / modify entry except role specified below.",
|
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||||
"fieldname": "acc_frozen_upto",
|
"fieldname": "acc_frozen_upto",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Accounts Frozen Upto"
|
"label": "Accounts Frozen Till Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||||
"fieldname": "frozen_accounts_modifier",
|
"fieldname": "frozen_accounts_modifier",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries",
|
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||||
"options": "Role"
|
"options": "Role"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Billing Address",
|
"default": "Billing Address",
|
||||||
"description": "Address used to determine Tax Category in transactions.",
|
"description": "Address used to determine Tax Category in transactions",
|
||||||
"fieldname": "determine_address_tax_category_from",
|
"fieldname": "determine_address_tax_category_from",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Determine Address Tax Category From",
|
"label": "Determine Address Tax Category From",
|
||||||
@ -75,7 +75,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Role that is allowed to submit transactions that exceed credit limits set.",
|
"description": "This role is allowed to submit transactions that exceed credit limits",
|
||||||
"fieldname": "credit_controller",
|
"fieldname": "credit_controller",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -127,7 +127,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "show_inclusive_tax_in_print",
|
"fieldname": "show_inclusive_tax_in_print",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Inclusive Tax In Print"
|
"label": "Show Inclusive Tax in Print"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
@ -165,7 +165,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Only select if you have setup Cash Flow Mapper documents",
|
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||||
"fieldname": "use_custom_cash_flow",
|
"fieldname": "use_custom_cash_flow",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Custom Cash Flow Format"
|
"label": "Use Custom Cash Flow Format"
|
||||||
@ -177,7 +177,7 @@
|
|||||||
"label": "Automatically Fetch Payment Terms"
|
"label": "Automatically Fetch Payment Terms"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Percentage you are allowed to bill more against the amount ordered. For example: If the order value is $100 for an item and tolerance is set as 10% then you are allowed to bill for $110.",
|
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||||
"fieldname": "over_billing_allowance",
|
"fieldname": "over_billing_allowance",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Over Billing Allowance (%)"
|
"label": "Over Billing Allowance (%)"
|
||||||
@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "If this is unchecked direct GL Entries will be created to book Deferred Revenue/Expense",
|
"description": "If this is unchecked, direct GL entries will be created to book deferred revenue or expense",
|
||||||
"fieldname": "book_deferred_entries_via_journal_entry",
|
"fieldname": "book_deferred_entries_via_journal_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Book Deferred Entries Via Journal Entry"
|
"label": "Book Deferred Entries Via Journal Entry"
|
||||||
@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Days",
|
"default": "Days",
|
||||||
"description": "If \"Months\" is selected then fixed amount will be booked as deferred revenue or expense for each month irrespective of number of days in a month. Will be prorated if deferred revenue or expense is not booked for an entire month.",
|
"description": "If \"Months\" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month",
|
||||||
"fieldname": "book_deferred_entries_based_on",
|
"fieldname": "book_deferred_entries_based_on",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Book Deferred Entries Based On",
|
"label": "Book Deferred Entries Based On",
|
||||||
@ -226,7 +226,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-07 14:58:50.325577",
|
"modified": "2020-10-13 11:32:52.268826",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
@ -254,4 +254,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,13 @@ class CashierClosing(Document):
|
|||||||
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
|
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
|
||||||
""", (self.date, self.from_time, self.time, self.user))
|
""", (self.date, self.from_time, self.time, self.user))
|
||||||
self.outstanding_amount = flt(values[0][0] if values else 0)
|
self.outstanding_amount = flt(values[0][0] if values else 0)
|
||||||
|
|
||||||
def make_calculations(self):
|
def make_calculations(self):
|
||||||
total = 0.00
|
total = 0.00
|
||||||
for i in self.payments:
|
for i in self.payments:
|
||||||
total += flt(i.amount)
|
total += flt(i.amount)
|
||||||
|
|
||||||
self.net_amount = total + self.outstanding_amount + self.expense - self.custody + self.returns
|
self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
|
||||||
|
|
||||||
def validate_time(self):
|
def validate_time(self):
|
||||||
if self.from_time >= self.time:
|
if self.from_time >= self.time:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:mode_of_payment",
|
"autoname": "field:mode_of_payment",
|
||||||
@ -28,7 +29,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"options": "Cash\nBank\nGeneral"
|
"options": "Cash\nBank\nGeneral\nPhone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "accounts",
|
"fieldname": "accounts",
|
||||||
@ -45,7 +46,9 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-credit-card",
|
"icon": "fa fa-credit-card",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2020-09-18 17:26:09.703215",
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-18 17:57:23.835236",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Mode of Payment",
|
"name": "Mode of Payment",
|
||||||
|
@ -6,7 +6,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
|||||||
frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) {
|
frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'name': ['in', 'Customer,Supplier']
|
'name': ['in', 'Customer, Supplier']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -14,29 +14,46 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
|||||||
if (frm.doc.company) {
|
if (frm.doc.company) {
|
||||||
frm.trigger('setup_company_filters');
|
frm.trigger('setup_company_filters');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.realtime.on('opening_invoice_creation_progress', data => {
|
||||||
|
if (!frm.doc.import_in_progress) {
|
||||||
|
frm.dashboard.reset();
|
||||||
|
frm.doc.import_in_progress = true;
|
||||||
|
}
|
||||||
|
if (data.user != frappe.session.user) return;
|
||||||
|
if (data.count == data.total) {
|
||||||
|
setTimeout((title) => {
|
||||||
|
frm.doc.import_in_progress = false;
|
||||||
|
frm.clear_table("invoices");
|
||||||
|
frm.refresh_fields();
|
||||||
|
frm.page.clear_indicator();
|
||||||
|
frm.dashboard.hide_progress(title);
|
||||||
|
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
|
||||||
|
}, 1500, data.title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
|
||||||
|
frm.page.set_indicator(__('In Progress'), 'orange');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.disable_save();
|
frm.disable_save();
|
||||||
frm.trigger("make_dashboard");
|
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||||
frm.page.set_primary_action(__('Create Invoices'), () => {
|
frm.page.set_primary_action(__('Create Invoices'), () => {
|
||||||
let btn_primary = frm.page.btn_primary.get(0);
|
let btn_primary = frm.page.btn_primary.get(0);
|
||||||
return frm.call({
|
return frm.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
freeze: true,
|
|
||||||
btn: $(btn_primary),
|
btn: $(btn_primary),
|
||||||
method: "make_invoices",
|
method: "make_invoices",
|
||||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||||
callback: (r) => {
|
|
||||||
if(!r.exc){
|
|
||||||
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
|
|
||||||
frm.clear_table("invoices");
|
|
||||||
frm.refresh_fields();
|
|
||||||
frm.reload_doc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (frm.doc.create_missing_party) {
|
||||||
|
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup_company_filters: function(frm) {
|
setup_company_filters: function(frm) {
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
import traceback
|
||||||
|
from json import dumps
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils.background_jobs import enqueue
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||||
|
|
||||||
|
|
||||||
@ -61,67 +64,48 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
prepare_invoice_summary(doctype, invoices)
|
prepare_invoice_summary(doctype, invoices)
|
||||||
|
|
||||||
return invoices_summary, max_count
|
return invoices_summary, max_count
|
||||||
|
|
||||||
def make_invoices(self):
|
def validate_company(self):
|
||||||
names = []
|
|
||||||
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
|
|
||||||
if not self.company:
|
if not self.company:
|
||||||
frappe.throw(_("Please select the Company"))
|
frappe.throw(_("Please select the Company"))
|
||||||
|
|
||||||
|
def set_missing_values(self, row):
|
||||||
|
row.qty = row.qty or 1.0
|
||||||
|
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
|
||||||
|
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
|
||||||
|
row.item_name = row.item_name or _("Opening Invoice Item")
|
||||||
|
row.posting_date = row.posting_date or nowdate()
|
||||||
|
row.due_date = row.due_date or nowdate()
|
||||||
|
|
||||||
company_details = frappe.get_cached_value('Company', self.company,
|
def validate_mandatory_invoice_fields(self, row):
|
||||||
["default_currency", "default_letter_head"], as_dict=1) or {}
|
if not frappe.db.exists(row.party_type, row.party):
|
||||||
|
if self.create_missing_party:
|
||||||
|
self.add_party(row.party_type, row.party)
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
|
||||||
|
|
||||||
|
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
|
||||||
|
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
|
||||||
|
if not row.get(scrub(d)):
|
||||||
|
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||||
|
|
||||||
|
def get_invoices(self):
|
||||||
|
invoices = []
|
||||||
for row in self.invoices:
|
for row in self.invoices:
|
||||||
if not row.qty:
|
if not row:
|
||||||
row.qty = 1.0
|
|
||||||
|
|
||||||
# always mandatory fields for the invoices
|
|
||||||
if not row.temporary_opening_account:
|
|
||||||
row.temporary_opening_account = get_temporary_opening_account(self.company)
|
|
||||||
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
|
|
||||||
|
|
||||||
# Allow to create invoice even if no party present in customer or supplier.
|
|
||||||
if not frappe.db.exists(row.party_type, row.party):
|
|
||||||
if self.create_missing_party:
|
|
||||||
self.add_party(row.party_type, row.party)
|
|
||||||
else:
|
|
||||||
frappe.throw(_("{0} {1} does not exist.").format(frappe.bold(row.party_type), frappe.bold(row.party)))
|
|
||||||
|
|
||||||
if not row.item_name:
|
|
||||||
row.item_name = _("Opening Invoice Item")
|
|
||||||
if not row.posting_date:
|
|
||||||
row.posting_date = nowdate()
|
|
||||||
if not row.due_date:
|
|
||||||
row.due_date = nowdate()
|
|
||||||
|
|
||||||
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
|
|
||||||
if not row.get(scrub(d)):
|
|
||||||
frappe.throw(mandatory_error_msg.format(row.idx, _(d), self.invoice_type))
|
|
||||||
|
|
||||||
args = self.get_invoice_dict(row=row)
|
|
||||||
if not args:
|
|
||||||
continue
|
continue
|
||||||
|
self.set_missing_values(row)
|
||||||
|
self.validate_mandatory_invoice_fields(row)
|
||||||
|
invoice = self.get_invoice_dict(row)
|
||||||
|
company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {}
|
||||||
if company_details:
|
if company_details:
|
||||||
args.update({
|
invoice.update({
|
||||||
"currency": company_details.get("default_currency"),
|
"currency": company_details.get("default_currency"),
|
||||||
"letter_head": company_details.get("default_letter_head")
|
"letter_head": company_details.get("default_letter_head")
|
||||||
})
|
})
|
||||||
|
invoices.append(invoice)
|
||||||
|
|
||||||
doc = frappe.get_doc(args).insert()
|
return invoices
|
||||||
doc.submit()
|
|
||||||
names.append(doc.name)
|
|
||||||
|
|
||||||
if len(self.invoices) > 5:
|
|
||||||
frappe.publish_realtime(
|
|
||||||
"progress", dict(
|
|
||||||
progress=[row.idx, len(self.invoices)],
|
|
||||||
title=_('Creating {0}').format(doc.doctype)
|
|
||||||
),
|
|
||||||
user=frappe.session.user
|
|
||||||
)
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def add_party(self, party_type, party):
|
def add_party(self, party_type, party):
|
||||||
party_doc = frappe.new_doc(party_type)
|
party_doc = frappe.new_doc(party_type)
|
||||||
@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
|
|
||||||
def get_invoice_dict(self, row=None):
|
def get_invoice_dict(self, row=None):
|
||||||
def get_item_dict():
|
def get_item_dict():
|
||||||
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center")
|
||||||
cost_center = row.get('cost_center') or frappe.get_cached_value('Company',
|
|
||||||
self.company, "cost_center")
|
|
||||||
|
|
||||||
if not cost_center:
|
if not cost_center:
|
||||||
frappe.throw(
|
frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)))
|
||||||
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
|
|
||||||
)
|
income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account"
|
||||||
|
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
||||||
rate = flt(row.outstanding_amount) / flt(row.qty)
|
rate = flt(row.outstanding_amount) / flt(row.qty)
|
||||||
|
|
||||||
return frappe._dict({
|
return frappe._dict({
|
||||||
@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
"cost_center": cost_center
|
"cost_center": cost_center
|
||||||
})
|
})
|
||||||
|
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
|
|
||||||
party_type = "Customer"
|
|
||||||
income_expense_account_field = "income_account"
|
|
||||||
if self.invoice_type == "Purchase":
|
|
||||||
party_type = "Supplier"
|
|
||||||
income_expense_account_field = "expense_account"
|
|
||||||
|
|
||||||
item = get_item_dict()
|
item = get_item_dict()
|
||||||
|
|
||||||
args = frappe._dict({
|
invoice = frappe._dict({
|
||||||
"items": [item],
|
"items": [item],
|
||||||
"is_opening": "Yes",
|
"is_opening": "Yes",
|
||||||
"set_posting_time": 1,
|
"set_posting_time": 1,
|
||||||
@ -180,21 +153,76 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"due_date": row.due_date,
|
"due_date": row.due_date,
|
||||||
"posting_date": row.posting_date,
|
"posting_date": row.posting_date,
|
||||||
frappe.scrub(party_type): row.party,
|
frappe.scrub(row.party_type): row.party,
|
||||||
|
"is_pos": 0,
|
||||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
|
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
|
||||||
})
|
})
|
||||||
|
|
||||||
accounting_dimension = get_accounting_dimensions()
|
accounting_dimension = get_accounting_dimensions()
|
||||||
|
|
||||||
for dimension in accounting_dimension:
|
for dimension in accounting_dimension:
|
||||||
args.update({
|
invoice.update({
|
||||||
dimension: item.get(dimension)
|
dimension: item.get(dimension)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.invoice_type == "Sales":
|
return invoice
|
||||||
args["is_pos"] = 0
|
|
||||||
|
|
||||||
return args
|
def make_invoices(self):
|
||||||
|
self.validate_company()
|
||||||
|
invoices = self.get_invoices()
|
||||||
|
if len(invoices) < 50:
|
||||||
|
return start_import(invoices)
|
||||||
|
else:
|
||||||
|
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||||
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
|
|
||||||
|
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||||
|
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||||
|
|
||||||
|
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||||
|
if self.name not in enqueued_jobs:
|
||||||
|
enqueue(
|
||||||
|
start_import,
|
||||||
|
queue="default",
|
||||||
|
timeout=6000,
|
||||||
|
event="opening_invoice_creation",
|
||||||
|
job_name=self.name,
|
||||||
|
invoices=invoices,
|
||||||
|
now=frappe.conf.developer_mode or frappe.flags.in_test
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_import(invoices):
|
||||||
|
errors = 0
|
||||||
|
names = []
|
||||||
|
for idx, d in enumerate(invoices):
|
||||||
|
try:
|
||||||
|
publish(idx, len(invoices), d.doctype)
|
||||||
|
doc = frappe.get_doc(d)
|
||||||
|
doc.insert()
|
||||||
|
doc.submit()
|
||||||
|
frappe.db.commit()
|
||||||
|
names.append(doc.name)
|
||||||
|
except Exception:
|
||||||
|
errors += 1
|
||||||
|
frappe.db.rollback()
|
||||||
|
message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
|
||||||
|
frappe.log_error(title="Error while creating Opening Invoice", message=message)
|
||||||
|
frappe.db.commit()
|
||||||
|
if errors:
|
||||||
|
frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
|
||||||
|
.format(errors, "<a href='#List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured"))
|
||||||
|
return names
|
||||||
|
|
||||||
|
def publish(index, total, doctype):
|
||||||
|
if total < 5: return
|
||||||
|
frappe.publish_realtime(
|
||||||
|
"opening_invoice_creation_progress",
|
||||||
|
dict(
|
||||||
|
title=_("Opening Invoice Creation In Progress"),
|
||||||
|
message=_('Creating {} out of {} {}').format(index + 1, total, doctype),
|
||||||
|
user=frappe.session.user,
|
||||||
|
count=index+1,
|
||||||
|
total=total
|
||||||
|
))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_temporary_opening_account(company=None):
|
def get_temporary_opening_account(company=None):
|
||||||
|
@ -44,7 +44,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
0: ["_Test Supplier", 300, "Overdue"],
|
0: ["_Test Supplier", 300, "Overdue"],
|
||||||
1: ["_Test Supplier 1", 250, "Overdue"],
|
1: ["_Test Supplier 1", 250, "Overdue"],
|
||||||
}
|
}
|
||||||
self.check_expected_values(invoices, expected_value, invoice_type="Purchase", )
|
self.check_expected_values(invoices, expected_value, "Purchase")
|
||||||
|
|
||||||
def get_opening_invoice_creation_dict(**args):
|
def get_opening_invoice_creation_dict(**args):
|
||||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||||
|
@ -1,313 +1,98 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
"creation": "2015-12-23 21:31:52.699821",
|
||||||
"allow_import": 0,
|
"doctype": "DocType",
|
||||||
"allow_rename": 0,
|
"editable_grid": 1,
|
||||||
"beta": 0,
|
"field_order": [
|
||||||
"creation": "2015-12-23 21:31:52.699821",
|
"payment_gateway",
|
||||||
"custom": 0,
|
"payment_channel",
|
||||||
"docstatus": 0,
|
"is_default",
|
||||||
"doctype": "DocType",
|
"column_break_4",
|
||||||
"document_type": "",
|
"payment_account",
|
||||||
"editable_grid": 1,
|
"currency",
|
||||||
|
"payment_request_message",
|
||||||
|
"message",
|
||||||
|
"message_examples"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "payment_gateway",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Link",
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_gateway",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Payment Gateway",
|
||||||
"label": "Payment Gateway",
|
"options": "Payment Gateway",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "Payment Gateway",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "is_default",
|
||||||
"bold": 0,
|
"fieldtype": "Check",
|
||||||
"collapsible": 0,
|
"label": "Is Default"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "is_default",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"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": "Is Default",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "column_break_4",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Column Break"
|
||||||
"bold": 0,
|
},
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_4",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "payment_account",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Link",
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_account",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Payment Account",
|
||||||
"label": "Payment Account",
|
"options": "Account",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "Account",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "payment_account.account_currency",
|
"fetch_from": "payment_account.account_currency",
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
"label": "Currency"
|
||||||
"ignore_user_permissions": 0,
|
},
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Currency",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "payment_request_message",
|
||||||
"bold": 0,
|
"fieldtype": "Section Break"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_request_message",
|
|
||||||
"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": "",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "Please click on the link below to make your payment",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "message",
|
||||||
"bold": 0,
|
"fieldtype": "Small Text",
|
||||||
"collapsible": 0,
|
"label": "Default Payment Request Message"
|
||||||
"columns": 0,
|
},
|
||||||
"default": "Please click on the link below to make your payment",
|
|
||||||
"fieldname": "message",
|
|
||||||
"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": "Default Payment Request Message",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "message_examples",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "HTML",
|
||||||
"bold": 0,
|
"label": "Message Examples",
|
||||||
"collapsible": 0,
|
"options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "message_examples",
|
{
|
||||||
"fieldtype": "HTML",
|
"default": "Email",
|
||||||
"hidden": 0,
|
"fieldname": "payment_channel",
|
||||||
"ignore_user_permissions": 0,
|
"fieldtype": "Select",
|
||||||
"ignore_xss_filter": 0,
|
"label": "Payment Channel",
|
||||||
"in_filter": 0,
|
"options": "\nEmail\nPhone"
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Message Examples",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n",
|
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_heading": 0,
|
"links": [],
|
||||||
"hide_toolbar": 0,
|
"modified": "2020-09-20 13:30:27.722852",
|
||||||
"idx": 0,
|
"modified_by": "Administrator",
|
||||||
"image_view": 0,
|
"module": "Accounts",
|
||||||
"in_create": 0,
|
"name": "Payment Gateway Account",
|
||||||
"is_submittable": 0,
|
"owner": "Administrator",
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2018-05-16 22:43:34.970491",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Accounts",
|
|
||||||
"name": "Payment Gateway Account",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
"create": 1,
|
||||||
"cancel": 0,
|
"delete": 1,
|
||||||
"create": 1,
|
"email": 1,
|
||||||
"delete": 1,
|
"export": 1,
|
||||||
"email": 1,
|
"print": 1,
|
||||||
"export": 1,
|
"read": 1,
|
||||||
"if_owner": 0,
|
"report": 1,
|
||||||
"import": 0,
|
"role": "Accounts Manager",
|
||||||
"permlevel": 0,
|
"share": 1,
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Accounts Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC"
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){
|
|||||||
})
|
})
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
||||||
if(frm.doc.payment_request_type == 'Inward' &&
|
if(frm.doc.payment_request_type == 'Inward' && frm.doc.payment_channel !== "Phone" &&
|
||||||
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
|
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
|
||||||
frm.add_custom_button(__('Resend Payment Email'), function(){
|
frm.add_custom_button(__('Resend Payment Email'), function(){
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"section_break_7",
|
"section_break_7",
|
||||||
"payment_gateway",
|
"payment_gateway",
|
||||||
"payment_account",
|
"payment_account",
|
||||||
|
"payment_channel",
|
||||||
"payment_order",
|
"payment_order",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
@ -230,6 +231,7 @@
|
|||||||
"label": "Recipient Message And Payment Details"
|
"label": "Recipient Message And Payment Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "print_format",
|
"fieldname": "print_format",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Print Format"
|
"label": "Print Format"
|
||||||
@ -241,6 +243,7 @@
|
|||||||
"label": "To"
|
"label": "To"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@ -277,16 +280,18 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.payment_request_type == 'Inward'",
|
"depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "section_break_10",
|
"fieldname": "section_break_10",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "message",
|
"fieldname": "message",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Message"
|
"label": "Message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "message_examples",
|
"fieldname": "message_examples",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Message Examples",
|
"label": "Message Examples",
|
||||||
@ -347,12 +352,21 @@
|
|||||||
"options": "Payment Request",
|
"options": "Payment Request",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "payment_gateway_account.payment_channel",
|
||||||
|
"fieldname": "payment_channel",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment Channel",
|
||||||
|
"options": "\nEmail\nPhone",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-07-17 14:06:42.185763",
|
"modified": "2020-09-18 12:24:14.178853",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
|
@ -36,7 +36,7 @@ class PaymentRequest(Document):
|
|||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
if (hasattr(ref_doc, "order_type") \
|
if (hasattr(ref_doc, "order_type") \
|
||||||
and getattr(ref_doc, "order_type") != "Shopping Cart"):
|
and getattr(ref_doc, "order_type") != "Shopping Cart"):
|
||||||
ref_amount = get_amount(ref_doc)
|
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||||
|
|
||||||
if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
|
if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
|
||||||
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
||||||
@ -76,11 +76,25 @@ class PaymentRequest(Document):
|
|||||||
or self.flags.mute_email:
|
or self.flags.mute_email:
|
||||||
send_mail = False
|
send_mail = False
|
||||||
|
|
||||||
if send_mail:
|
if send_mail and self.payment_channel != "Phone":
|
||||||
self.set_payment_request_url()
|
self.set_payment_request_url()
|
||||||
self.send_email()
|
self.send_email()
|
||||||
self.make_communication_entry()
|
self.make_communication_entry()
|
||||||
|
|
||||||
|
elif self.payment_channel == "Phone":
|
||||||
|
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||||
|
payment_record = dict(
|
||||||
|
reference_doctype="Payment Request",
|
||||||
|
reference_docname=self.name,
|
||||||
|
payment_reference=self.reference_name,
|
||||||
|
grand_total=self.grand_total,
|
||||||
|
sender=self.email_to,
|
||||||
|
currency=self.currency,
|
||||||
|
payment_gateway=self.payment_gateway
|
||||||
|
)
|
||||||
|
controller.validate_transaction_currency(self.currency)
|
||||||
|
controller.request_for_payment(**payment_record)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.check_if_payment_entry_exists()
|
self.check_if_payment_entry_exists()
|
||||||
self.set_as_cancelled()
|
self.set_as_cancelled()
|
||||||
@ -105,13 +119,14 @@ class PaymentRequest(Document):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def set_payment_request_url(self):
|
def set_payment_request_url(self):
|
||||||
if self.payment_account:
|
if self.payment_account and self.payment_channel != "Phone":
|
||||||
self.payment_url = self.get_payment_url()
|
self.payment_url = self.get_payment_url()
|
||||||
|
|
||||||
if self.payment_url:
|
if self.payment_url:
|
||||||
self.db_set('payment_url', self.payment_url)
|
self.db_set('payment_url', self.payment_url)
|
||||||
|
|
||||||
if self.payment_url or not self.payment_gateway_account:
|
if self.payment_url or not self.payment_gateway_account \
|
||||||
|
or (self.payment_gateway_account and self.payment_channel == "Phone"):
|
||||||
self.db_set('status', 'Initiated')
|
self.db_set('status', 'Initiated')
|
||||||
|
|
||||||
def get_payment_url(self):
|
def get_payment_url(self):
|
||||||
@ -140,10 +155,14 @@ class PaymentRequest(Document):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def set_as_paid(self):
|
def set_as_paid(self):
|
||||||
payment_entry = self.create_payment_entry()
|
if self.payment_channel == "Phone":
|
||||||
self.make_invoice()
|
self.db_set("status", "Paid")
|
||||||
|
|
||||||
return payment_entry
|
else:
|
||||||
|
payment_entry = self.create_payment_entry()
|
||||||
|
self.make_invoice()
|
||||||
|
|
||||||
|
return payment_entry
|
||||||
|
|
||||||
def create_payment_entry(self, submit=True):
|
def create_payment_entry(self, submit=True):
|
||||||
"""create entry"""
|
"""create entry"""
|
||||||
@ -151,7 +170,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
|
|
||||||
if self.reference_doctype == "Sales Invoice":
|
if self.reference_doctype in ["Sales Invoice", "POS Invoice"]:
|
||||||
party_account = ref_doc.debit_to
|
party_account = ref_doc.debit_to
|
||||||
elif self.reference_doctype == "Purchase Invoice":
|
elif self.reference_doctype == "Purchase Invoice":
|
||||||
party_account = ref_doc.credit_to
|
party_account = ref_doc.credit_to
|
||||||
@ -166,8 +185,8 @@ class PaymentRequest(Document):
|
|||||||
else:
|
else:
|
||||||
party_amount = self.grand_total
|
party_amount = self.grand_total
|
||||||
|
|
||||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name,
|
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
|
||||||
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount)
|
bank_account=self.payment_account, bank_amount=bank_amount)
|
||||||
|
|
||||||
payment_entry.update({
|
payment_entry.update({
|
||||||
"reference_no": self.name,
|
"reference_no": self.name,
|
||||||
@ -255,7 +274,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
# if shopping cart enabled and in session
|
# if shopping cart enabled and in session
|
||||||
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
||||||
and frappe.local.session.user != "Guest"):
|
and frappe.local.session.user != "Guest") and self.payment_channel != "Phone":
|
||||||
|
|
||||||
success_url = shopping_cart_settings.payment_success_url
|
success_url = shopping_cart_settings.payment_success_url
|
||||||
if success_url:
|
if success_url:
|
||||||
@ -280,7 +299,9 @@ def make_payment_request(**args):
|
|||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||||
grand_total = get_amount(ref_doc)
|
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||||
|
|
||||||
|
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||||
if args.loyalty_points and args.dt == "Sales Order":
|
if args.loyalty_points and args.dt == "Sales Order":
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||||
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
|
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
|
||||||
@ -288,8 +309,6 @@ def make_payment_request(**args):
|
|||||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||||
grand_total = grand_total - loyalty_amount
|
grand_total = grand_total - loyalty_amount
|
||||||
|
|
||||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
|
||||||
|
|
||||||
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
|
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
|
||||||
if args.get('party_type') else '')
|
if args.get('party_type') else '')
|
||||||
|
|
||||||
@ -314,9 +333,11 @@ def make_payment_request(**args):
|
|||||||
"payment_gateway_account": gateway_account.get("name"),
|
"payment_gateway_account": gateway_account.get("name"),
|
||||||
"payment_gateway": gateway_account.get("payment_gateway"),
|
"payment_gateway": gateway_account.get("payment_gateway"),
|
||||||
"payment_account": gateway_account.get("payment_account"),
|
"payment_account": gateway_account.get("payment_account"),
|
||||||
|
"payment_channel": gateway_account.get("payment_channel"),
|
||||||
"payment_request_type": args.get("payment_request_type"),
|
"payment_request_type": args.get("payment_request_type"),
|
||||||
"currency": ref_doc.currency,
|
"currency": ref_doc.currency,
|
||||||
"grand_total": grand_total,
|
"grand_total": grand_total,
|
||||||
|
"mode_of_payment": args.mode_of_payment,
|
||||||
"email_to": args.recipient_id or ref_doc.owner,
|
"email_to": args.recipient_id or ref_doc.owner,
|
||||||
"subject": _("Payment Request for {0}").format(args.dn),
|
"subject": _("Payment Request for {0}").format(args.dn),
|
||||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||||
@ -344,7 +365,7 @@ def make_payment_request(**args):
|
|||||||
|
|
||||||
return pr.as_dict()
|
return pr.as_dict()
|
||||||
|
|
||||||
def get_amount(ref_doc):
|
def get_amount(ref_doc, payment_account=None):
|
||||||
"""get amount based on doctype"""
|
"""get amount based on doctype"""
|
||||||
dt = ref_doc.doctype
|
dt = ref_doc.doctype
|
||||||
if dt in ["Sales Order", "Purchase Order"]:
|
if dt in ["Sales Order", "Purchase Order"]:
|
||||||
@ -356,6 +377,12 @@ def get_amount(ref_doc):
|
|||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||||
|
|
||||||
|
elif dt == "POS Invoice":
|
||||||
|
for pay in ref_doc.payments:
|
||||||
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
|
grand_total = pay.amount
|
||||||
|
break
|
||||||
|
|
||||||
elif dt == "Fees":
|
elif dt == "Fees":
|
||||||
grand_total = ref_doc.outstanding_amount
|
grand_total = ref_doc.outstanding_amount
|
||||||
|
|
||||||
@ -366,6 +393,10 @@ def get_amount(ref_doc):
|
|||||||
frappe.throw(_("Payment Entry is already created"))
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
|
|
||||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||||
|
"""
|
||||||
|
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
||||||
|
and get the summation of existing paid payment request for Phone payment channel.
|
||||||
|
"""
|
||||||
existing_payment_request_amount = frappe.db.sql("""
|
existing_payment_request_amount = frappe.db.sql("""
|
||||||
select sum(grand_total)
|
select sum(grand_total)
|
||||||
from `tabPayment Request`
|
from `tabPayment Request`
|
||||||
@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
|||||||
reference_doctype = %s
|
reference_doctype = %s
|
||||||
and reference_name = %s
|
and reference_name = %s
|
||||||
and docstatus = 1
|
and docstatus = 1
|
||||||
and status != 'Paid'
|
and (status != 'Paid'
|
||||||
|
or (payment_channel = 'Phone'
|
||||||
|
and status = 'Paid'))
|
||||||
""", (ref_dt, ref_dn))
|
""", (ref_dt, ref_dn))
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||||
|
|
||||||
|
@ -201,5 +201,22 @@ frappe.ui.form.on('POS Invoice', {
|
|||||||
}
|
}
|
||||||
frm.set_value("loyalty_amount", loyalty_amount);
|
frm.set_value("loyalty_amount", loyalty_amount);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
request_for_payment: function (frm) {
|
||||||
|
frm.save().then(() => {
|
||||||
|
frappe.dom.freeze();
|
||||||
|
frappe.call({
|
||||||
|
method: 'create_payment_request',
|
||||||
|
doc: frm.doc,
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
frappe.msgprint('Payment request failed');
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
frappe.msgprint('Payment request sent successfully');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -279,8 +279,7 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Return (Credit Note)",
|
"label": "Is Return (Credit Note)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break1",
|
"fieldname": "column_break1",
|
||||||
@ -461,7 +460,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "contact_mobile",
|
"fieldname": "contact_mobile",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Mobile No",
|
"label": "Mobile No",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@ -1579,10 +1578,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-07 12:43:09.138720",
|
"modified": "2020-09-28 16:51:24.641755",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
@ -15,6 +15,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
|
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.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 six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
against_psi_doc.make_loyalty_point_entry()
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
if self.redeem_loyalty_points and self.loyalty_points:
|
if self.redeem_loyalty_points and self.loyalty_points:
|
||||||
self.apply_loyalty_points()
|
self.apply_loyalty_points()
|
||||||
|
self.check_phone_payments()
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
@ -69,6 +71,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
against_psi_doc.delete_loyalty_point_entry()
|
against_psi_doc.delete_loyalty_point_entry()
|
||||||
against_psi_doc.make_loyalty_point_entry()
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
|
|
||||||
|
def check_phone_payments(self):
|
||||||
|
for pay in self.payments:
|
||||||
|
if pay.type == "Phone" and pay.amount >= 0:
|
||||||
|
paid_amt = frappe.db.get_value("Payment Request",
|
||||||
|
filters=dict(
|
||||||
|
reference_doctype="POS Invoice", reference_name=self.name,
|
||||||
|
mode_of_payment=pay.mode_of_payment, status="Paid"),
|
||||||
|
fieldname="grand_total")
|
||||||
|
|
||||||
|
if pay.amount != paid_amt:
|
||||||
|
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||||
|
|
||||||
@ -312,6 +326,32 @@ class POSInvoice(SalesInvoice):
|
|||||||
if not pay.account:
|
if not pay.account:
|
||||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||||
|
|
||||||
|
def create_payment_request(self):
|
||||||
|
for pay in self.payments:
|
||||||
|
if pay.type == "Phone":
|
||||||
|
if pay.amount <= 0:
|
||||||
|
frappe.throw(_("Payment amount cannot be less than or equal to 0"))
|
||||||
|
|
||||||
|
if not self.contact_mobile:
|
||||||
|
frappe.throw(_("Please enter the phone number first"))
|
||||||
|
|
||||||
|
payment_gateway = frappe.db.get_value("Payment Gateway Account", {
|
||||||
|
"payment_account": pay.account,
|
||||||
|
})
|
||||||
|
record = {
|
||||||
|
"payment_gateway": payment_gateway,
|
||||||
|
"dt": "POS Invoice",
|
||||||
|
"dn": self.name,
|
||||||
|
"payment_request_type": "Inward",
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"mode_of_payment": pay.mode_of_payment,
|
||||||
|
"recipient_id": self.contact_mobile,
|
||||||
|
"submit_doc": True
|
||||||
|
}
|
||||||
|
|
||||||
|
return make_payment_request(**record)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||||
|
@ -243,7 +243,11 @@ def check_amount_vs_description(amount_matching, description_matching):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if "reference_no" in am_match and "reference_no" in des_match:
|
if "reference_no" in am_match and "reference_no" in des_match:
|
||||||
if difflib.SequenceMatcher(lambda x: x == " ", am_match["reference_no"], des_match["reference_no"]).ratio() > 70:
|
# Sequence Matcher does not handle None as input
|
||||||
|
am_reference = am_match["reference_no"] or ""
|
||||||
|
des_reference = des_match["reference_no"] or ""
|
||||||
|
|
||||||
|
if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70:
|
||||||
if am_match not in result:
|
if am_match not in result:
|
||||||
result.append(am_match)
|
result.append(am_match)
|
||||||
if result:
|
if result:
|
||||||
|
@ -796,7 +796,7 @@ def get_children(doctype, parent, company, is_root=False):
|
|||||||
|
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
def create_payment_gateway_account(gateway):
|
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
||||||
|
|
||||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||||
@ -831,7 +831,8 @@ def create_payment_gateway_account(gateway):
|
|||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"payment_gateway": gateway,
|
"payment_gateway": gateway,
|
||||||
"payment_account": bank_account.name,
|
"payment_account": bank_account.name,
|
||||||
"currency": bank_account.account_currency
|
"currency": bank_account.account_currency,
|
||||||
|
"payment_channel": payment_channel
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
|
@ -46,26 +46,26 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "po_required",
|
"fieldname": "po_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Purchase Order Required for Purchase Invoice & Receipt Creation",
|
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "pr_required",
|
"fieldname": "pr_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Purchase Receipt Required for Purchase Invoice Creation",
|
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "maintain_same_rate",
|
"fieldname": "maintain_same_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Maintain same rate throughout purchase cycle"
|
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_multiple_items",
|
"fieldname": "allow_multiple_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Item to be added multiple times in a transaction"
|
"label": "Allow Item To Be Added Multiple Times in a Transaction"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "subcontract",
|
"fieldname": "subcontract",
|
||||||
@ -93,9 +93,10 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-15 14:49:32.513611",
|
"modified": "2020-10-13 12:00:23.276329",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
@ -113,4 +114,4 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC"
|
||||||
}
|
}
|
||||||
|
@ -29,14 +29,12 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
|
|
||||||
refresh: function(frm, cdt, cdn) {
|
refresh: function(frm, cdt, cdn) {
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.add_custom_button(__('Create'),
|
|
||||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Supplier Quotation"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("View"),
|
frm.add_custom_button(__('Supplier Quotation'),
|
||||||
function(){ frappe.set_route('List', 'Supplier Quotation',
|
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
||||||
{'request_for_quotation': frm.doc.name}) }, __("Supplier Quotation"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("Send Supplier Emails"), function() {
|
|
||||||
|
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
||||||
freeze: true,
|
freeze: true,
|
||||||
@ -47,150 +45,82 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, __("Tools"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Download PDF'), () => {
|
||||||
|
var suppliers = [];
|
||||||
|
const fields = [{
|
||||||
|
fieldtype: 'Link',
|
||||||
|
label: __('Select a Supplier'),
|
||||||
|
fieldname: 'supplier',
|
||||||
|
options: 'Supplier',
|
||||||
|
reqd: 1,
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: [
|
||||||
|
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
frappe.prompt(fields, data => {
|
||||||
|
var child = locals[cdt][cdn]
|
||||||
|
|
||||||
|
var w = window.open(
|
||||||
|
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
|
||||||
|
+"doctype="+encodeURIComponent(frm.doc.doctype)
|
||||||
|
+"&name="+encodeURIComponent(frm.doc.name)
|
||||||
|
+"&supplier="+encodeURIComponent(data.supplier)
|
||||||
|
+"&no_letterhead=0"));
|
||||||
|
if(!w) {
|
||||||
|
frappe.msgprint(__("Please enable pop-ups")); return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Download PDF for Supplier',
|
||||||
|
'Download');
|
||||||
|
},
|
||||||
|
__("Tools"));
|
||||||
|
|
||||||
|
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get_suppliers_button: function (frm) {
|
|
||||||
var doc = frm.doc;
|
|
||||||
var dialog = new frappe.ui.Dialog({
|
|
||||||
title: __("Get Suppliers"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
"fieldtype": "Select", "label": __("Get Suppliers By"),
|
|
||||||
"fieldname": "search_type",
|
|
||||||
"options": ["Tag","Supplier Group"],
|
|
||||||
"reqd": 1,
|
|
||||||
onchange() {
|
|
||||||
if(dialog.get_value('search_type') == 'Tag'){
|
|
||||||
frappe.call({
|
|
||||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_tag',
|
|
||||||
}).then(r => {
|
|
||||||
dialog.set_df_property("tag", "options", r.message)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Link", "label": __("Supplier Group"),
|
|
||||||
"fieldname": "supplier_group",
|
|
||||||
"options": "Supplier Group",
|
|
||||||
"reqd": 0,
|
|
||||||
"depends_on": "eval:doc.search_type == 'Supplier Group'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Select", "label": __("Tag"),
|
|
||||||
"fieldname": "tag",
|
|
||||||
"reqd": 0,
|
|
||||||
"depends_on": "eval:doc.search_type == 'Tag'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Button", "label": __("Add All Suppliers"),
|
|
||||||
"fieldname": "add_suppliers"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.fields_dict.add_suppliers.$input.click(function() {
|
|
||||||
var args = dialog.get_values();
|
|
||||||
if(!args) return;
|
|
||||||
dialog.hide();
|
|
||||||
|
|
||||||
//Remove blanks
|
|
||||||
for (var j = 0; j < frm.doc.suppliers.length; j++) {
|
|
||||||
if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) {
|
|
||||||
frm.get_field("suppliers").grid.grid_rows[j].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_suppliers(r) {
|
|
||||||
if(r.message) {
|
|
||||||
for (var i = 0; i < r.message.length; i++) {
|
|
||||||
var exists = false;
|
|
||||||
if (r.message[i].constructor === Array){
|
|
||||||
var supplier = r.message[i][0];
|
|
||||||
} else {
|
|
||||||
var supplier = r.message[i].name;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var j = 0; j < doc.suppliers.length;j++) {
|
|
||||||
if (supplier === doc.suppliers[j].supplier) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!exists) {
|
|
||||||
var d = frm.add_child('suppliers');
|
|
||||||
d.supplier = supplier;
|
|
||||||
frm.script_manager.trigger("supplier", d.doctype, d.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frm.refresh_field("suppliers");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.search_type === "Tag" && args.tag) {
|
|
||||||
return frappe.call({
|
|
||||||
type: "GET",
|
|
||||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
|
||||||
args: {
|
|
||||||
"doctype": "Supplier",
|
|
||||||
"tag": args.tag
|
|
||||||
},
|
|
||||||
callback: load_suppliers
|
|
||||||
});
|
|
||||||
} else if (args.supplier_group) {
|
|
||||||
return frappe.call({
|
|
||||||
method: "frappe.client.get_list",
|
|
||||||
args: {
|
|
||||||
doctype: "Supplier",
|
|
||||||
order_by: "name",
|
|
||||||
fields: ["name"],
|
|
||||||
filters: [["Supplier", "supplier_group", "=", args.supplier_group]]
|
|
||||||
|
|
||||||
},
|
|
||||||
callback: load_suppliers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
},
|
|
||||||
make_suppplier_quotation: function(frm) {
|
make_suppplier_quotation: function(frm) {
|
||||||
var doc = frm.doc;
|
var doc = frm.doc;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("For Supplier"),
|
title: __("Create Supplier Quotation"),
|
||||||
fields: [
|
fields: [
|
||||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
{ "fieldtype": "Select", "label": __("Supplier"),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"options": doc.suppliers.map(d => d.supplier),
|
"options": doc.suppliers.map(d => d.supplier),
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
||||||
{ "fieldtype": "Button", "label": __('Create Supplier Quotation'),
|
],
|
||||||
"fieldname": "make_supplier_quotation", "cssClass": "btn-primary" },
|
primary_action_label: __("Create"),
|
||||||
]
|
primary_action: (args) => {
|
||||||
|
if(!args) return;
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
return frappe.call({
|
||||||
|
type: "GET",
|
||||||
|
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
||||||
|
args: {
|
||||||
|
"source_name": doc.name,
|
||||||
|
"for_supplier": args.supplier
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.fields_dict.make_supplier_quotation.$input.click(function() {
|
|
||||||
var args = dialog.get_values();
|
|
||||||
if(!args) return;
|
|
||||||
dialog.hide();
|
|
||||||
return frappe.call({
|
|
||||||
type: "GET",
|
|
||||||
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
|
||||||
args: {
|
|
||||||
"source_name": doc.name,
|
|
||||||
"for_supplier": args.supplier
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function(r) {
|
|
||||||
if(!r.exc) {
|
|
||||||
var doc = frappe.model.sync(r.message);
|
|
||||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -273,42 +203,6 @@ frappe.ui.form.on("Request for Quotation Supplier",{
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
download_pdf: function(frm, cdt, cdn) {
|
|
||||||
var child = locals[cdt][cdn]
|
|
||||||
|
|
||||||
var w = window.open(
|
|
||||||
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
|
|
||||||
+"doctype="+encodeURIComponent(frm.doc.doctype)
|
|
||||||
+"&name="+encodeURIComponent(frm.doc.name)
|
|
||||||
+"&supplier_idx="+encodeURIComponent(child.idx)
|
|
||||||
+"&no_letterhead=0"));
|
|
||||||
if(!w) {
|
|
||||||
frappe.msgprint(__("Please enable pop-ups")); return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
no_quote: function(frm, cdt, cdn) {
|
|
||||||
var d = locals[cdt][cdn];
|
|
||||||
if (d.no_quote) {
|
|
||||||
if (d.quote_status != __('Received')) {
|
|
||||||
frappe.model.set_value(cdt, cdn, 'quote_status', 'No Quote');
|
|
||||||
} else {
|
|
||||||
frappe.msgprint(__("Cannot set a received RFQ to No Quote"));
|
|
||||||
frappe.model.set_value(cdt, cdn, 'no_quote', 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
d.quote_status = __('Pending');
|
|
||||||
frm.call({
|
|
||||||
method:"update_rfq_supplier_status",
|
|
||||||
doc: frm.doc,
|
|
||||||
args: {
|
|
||||||
sup_name: d.supplier
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
frm.refresh_field("suppliers");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({
|
erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({
|
||||||
@ -332,7 +226,8 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
per_ordered: ["<", 99.99]
|
per_ordered: ["<", 99.99]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
// Get items from Opportunity
|
// Get items from Opportunity
|
||||||
this.frm.add_custom_button(__('Opportunity'),
|
this.frm.add_custom_button(__('Opportunity'),
|
||||||
function() {
|
function() {
|
||||||
@ -344,7 +239,8 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
company: me.frm.doc.company
|
company: me.frm.doc.company
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
// Get items from open Material Requests based on supplier
|
// Get items from open Material Requests based on supplier
|
||||||
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
||||||
// Create a dialog window for the user to pick their supplier
|
// Create a dialog window for the user to pick their supplier
|
||||||
@ -382,8 +278,13 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.show();
|
d.show();
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
|
// Get Suppliers
|
||||||
|
this.frm.add_custom_button(__('Get Suppliers'),
|
||||||
|
function() {
|
||||||
|
me.get_suppliers_button(me.frm);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -393,9 +294,108 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
|
|
||||||
tc_name: function() {
|
tc_name: function() {
|
||||||
this.get_terms();
|
this.get_terms();
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
|
||||||
|
get_suppliers_button: function (frm) {
|
||||||
|
var doc = frm.doc;
|
||||||
|
var dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Get Suppliers"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
"fieldtype": "Select", "label": __("Get Suppliers By"),
|
||||||
|
"fieldname": "search_type",
|
||||||
|
"options": ["Tag","Supplier Group"],
|
||||||
|
"reqd": 1,
|
||||||
|
onchange() {
|
||||||
|
if(dialog.get_value('search_type') == 'Tag'){
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_tag',
|
||||||
|
}).then(r => {
|
||||||
|
dialog.set_df_property("tag", "options", r.message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldtype": "Link", "label": __("Supplier Group"),
|
||||||
|
"fieldname": "supplier_group",
|
||||||
|
"options": "Supplier Group",
|
||||||
|
"reqd": 0,
|
||||||
|
"depends_on": "eval:doc.search_type == 'Supplier Group'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldtype": "Select", "label": __("Tag"),
|
||||||
|
"fieldname": "tag",
|
||||||
|
"reqd": 0,
|
||||||
|
"depends_on": "eval:doc.search_type == 'Tag'",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action_label: __("Add Suppliers"),
|
||||||
|
primary_action : (args) => {
|
||||||
|
if(!args) return;
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
//Remove blanks
|
||||||
|
for (var j = 0; j < frm.doc.suppliers.length; j++) {
|
||||||
|
if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) {
|
||||||
|
frm.get_field("suppliers").grid.grid_rows[j].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_suppliers(r) {
|
||||||
|
if(r.message) {
|
||||||
|
for (var i = 0; i < r.message.length; i++) {
|
||||||
|
var exists = false;
|
||||||
|
if (r.message[i].constructor === Array){
|
||||||
|
var supplier = r.message[i][0];
|
||||||
|
} else {
|
||||||
|
var supplier = r.message[i].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < doc.suppliers.length;j++) {
|
||||||
|
if (supplier === doc.suppliers[j].supplier) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!exists) {
|
||||||
|
var d = frm.add_child('suppliers');
|
||||||
|
d.supplier = supplier;
|
||||||
|
frm.script_manager.trigger("supplier", d.doctype, d.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frm.refresh_field("suppliers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.search_type === "Tag" && args.tag) {
|
||||||
|
return frappe.call({
|
||||||
|
type: "GET",
|
||||||
|
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||||
|
args: {
|
||||||
|
"doctype": "Supplier",
|
||||||
|
"tag": args.tag
|
||||||
|
},
|
||||||
|
callback: load_suppliers
|
||||||
|
});
|
||||||
|
} else if (args.supplier_group) {
|
||||||
|
return frappe.call({
|
||||||
|
method: "frappe.client.get_list",
|
||||||
|
args: {
|
||||||
|
doctype: "Supplier",
|
||||||
|
order_by: "name",
|
||||||
|
fields: ["name"],
|
||||||
|
filters: [["Supplier", "supplier_group", "=", args.supplier_group]]
|
||||||
|
|
||||||
|
},
|
||||||
|
callback: load_suppliers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// for backward compatibility: combine new and previous states
|
||||||
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
||||||
|
@ -12,9 +12,10 @@
|
|||||||
"vendor",
|
"vendor",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"status",
|
||||||
|
"amended_from",
|
||||||
"suppliers_section",
|
"suppliers_section",
|
||||||
"suppliers",
|
"suppliers",
|
||||||
"get_suppliers_button",
|
|
||||||
"items_section",
|
"items_section",
|
||||||
"items",
|
"items",
|
||||||
"link_to_mrs",
|
"link_to_mrs",
|
||||||
@ -31,11 +32,7 @@
|
|||||||
"terms",
|
"terms",
|
||||||
"printing_settings",
|
"printing_settings",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"letter_head",
|
"letter_head"
|
||||||
"more_info",
|
|
||||||
"status",
|
|
||||||
"column_break3",
|
|
||||||
"amended_from"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -83,6 +80,7 @@
|
|||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "Today",
|
||||||
"fieldname": "transaction_date",
|
"fieldname": "transaction_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -99,16 +97,11 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "suppliers",
|
"fieldname": "suppliers",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Supplier Detail",
|
"label": "Suppliers",
|
||||||
"options": "Request for Quotation Supplier",
|
"options": "Request for Quotation Supplier",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "get_suppliers_button",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Get Suppliers"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "items_section",
|
"fieldname": "items_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -144,6 +137,7 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fetch_from": "email_template.response",
|
"fetch_from": "email_template.response",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "message_for_supplier",
|
"fieldname": "message_for_supplier",
|
||||||
@ -206,14 +200,6 @@
|
|||||||
"options": "Letter Head",
|
"options": "Letter Head",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "more_info",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "More Information",
|
|
||||||
"oldfieldtype": "Section Break",
|
|
||||||
"options": "fa fa-file-text"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@ -227,10 +213,6 @@
|
|||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -275,9 +257,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-01 14:54:50.888729",
|
"modified": "2020-10-16 17:49:09.561929",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
@ -28,6 +28,10 @@ class RequestforQuotation(BuyingController):
|
|||||||
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
||||||
self.update_email_id()
|
self.update_email_id()
|
||||||
|
|
||||||
|
if self.docstatus < 1:
|
||||||
|
# after amend and save, status still shows as cancelled, until submit
|
||||||
|
frappe.db.set(self, 'status', 'Draft')
|
||||||
|
|
||||||
def validate_duplicate_supplier(self):
|
def validate_duplicate_supplier(self):
|
||||||
supplier_list = [d.supplier for d in self.suppliers]
|
supplier_list = [d.supplier for d in self.suppliers]
|
||||||
if len(supplier_list) != len(set(supplier_list)):
|
if len(supplier_list) != len(set(supplier_list)):
|
||||||
@ -82,7 +86,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
# make new user if required
|
# make new user if required
|
||||||
update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link())
|
update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link())
|
||||||
|
|
||||||
self.update_supplier_part_no(rfq_supplier)
|
self.update_supplier_part_no(rfq_supplier.supplier)
|
||||||
self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
|
self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
|
||||||
rfq_supplier.email_sent = 1
|
rfq_supplier.email_sent = 1
|
||||||
if not rfq_supplier.contact:
|
if not rfq_supplier.contact:
|
||||||
@ -93,11 +97,11 @@ class RequestforQuotation(BuyingController):
|
|||||||
# RFQ link for supplier portal
|
# RFQ link for supplier portal
|
||||||
return get_url("/rfq/" + self.name)
|
return get_url("/rfq/" + self.name)
|
||||||
|
|
||||||
def update_supplier_part_no(self, args):
|
def update_supplier_part_no(self, supplier):
|
||||||
self.vendor = args.supplier
|
self.vendor = supplier
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
item.supplier_part_no = frappe.db.get_value('Item Supplier',
|
item.supplier_part_no = frappe.db.get_value('Item Supplier',
|
||||||
{'parent': item.item_code, 'supplier': args.supplier}, 'supplier_part_no')
|
{'parent': item.item_code, 'supplier': supplier}, 'supplier_part_no')
|
||||||
|
|
||||||
def update_supplier_contact(self, rfq_supplier, link):
|
def update_supplier_contact(self, rfq_supplier, link):
|
||||||
'''Create a new user for the supplier if not set in contact'''
|
'''Create a new user for the supplier if not set in contact'''
|
||||||
@ -197,23 +201,22 @@ class RequestforQuotation(BuyingController):
|
|||||||
def update_rfq_supplier_status(self, sup_name=None):
|
def update_rfq_supplier_status(self, sup_name=None):
|
||||||
for supplier in self.suppliers:
|
for supplier in self.suppliers:
|
||||||
if sup_name == None or supplier.supplier == sup_name:
|
if sup_name == None or supplier.supplier == sup_name:
|
||||||
if supplier.quote_status != _('No Quote'):
|
quote_status = _('Received')
|
||||||
quote_status = _('Received')
|
for item in self.items:
|
||||||
for item in self.items:
|
sqi_count = frappe.db.sql("""
|
||||||
sqi_count = frappe.db.sql("""
|
SELECT
|
||||||
SELECT
|
COUNT(sqi.name) as count
|
||||||
COUNT(sqi.name) as count
|
FROM
|
||||||
FROM
|
`tabSupplier Quotation Item` as sqi,
|
||||||
`tabSupplier Quotation Item` as sqi,
|
`tabSupplier Quotation` as sq
|
||||||
`tabSupplier Quotation` as sq
|
WHERE sq.supplier = %(supplier)s
|
||||||
WHERE sq.supplier = %(supplier)s
|
AND sqi.docstatus = 1
|
||||||
AND sqi.docstatus = 1
|
AND sqi.request_for_quotation_item = %(rqi)s
|
||||||
AND sqi.request_for_quotation_item = %(rqi)s
|
AND sqi.parent = sq.name""",
|
||||||
AND sqi.parent = sq.name""",
|
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
||||||
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
if (sqi_count.count) == 0:
|
||||||
if (sqi_count.count) == 0:
|
quote_status = _('Pending')
|
||||||
quote_status = _('Pending')
|
supplier.quote_status = quote_status
|
||||||
supplier.quote_status = quote_status
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -322,16 +325,15 @@ def create_rfq_items(sq_doc, supplier, data):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pdf(doctype, name, supplier_idx):
|
def get_pdf(doctype, name, supplier):
|
||||||
doc = get_rfq_doc(doctype, name, supplier_idx)
|
doc = get_rfq_doc(doctype, name, supplier)
|
||||||
if doc:
|
if doc:
|
||||||
download_pdf(doctype, name, doc=doc)
|
download_pdf(doctype, name, doc=doc)
|
||||||
|
|
||||||
def get_rfq_doc(doctype, name, supplier_idx):
|
def get_rfq_doc(doctype, name, supplier):
|
||||||
if cint(supplier_idx):
|
if supplier:
|
||||||
doc = frappe.get_doc(doctype, name)
|
doc = frappe.get_doc(doctype, name)
|
||||||
args = doc.get('suppliers')[cint(supplier_idx) - 1]
|
doc.update_supplier_part_no(supplier)
|
||||||
doc.update_supplier_part_no(args)
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -25,14 +25,10 @@ class TestRequestforQuotation(unittest.TestCase):
|
|||||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
||||||
sq.submit()
|
sq.submit()
|
||||||
|
|
||||||
# No Quote first supplier quotation
|
|
||||||
rfq.get('suppliers')[1].no_quote = 1
|
|
||||||
rfq.get('suppliers')[1].quote_status = 'No Quote'
|
|
||||||
|
|
||||||
rfq.update_rfq_supplier_status() #rfq.get('suppliers')[1].supplier)
|
rfq.update_rfq_supplier_status() #rfq.get('suppliers')[1].supplier)
|
||||||
|
|
||||||
self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Received')
|
self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Received')
|
||||||
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote')
|
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending')
|
||||||
|
|
||||||
def test_make_supplier_quotation(self):
|
def test_make_supplier_quotation(self):
|
||||||
rfq = make_request_for_quotation()
|
rfq = make_request_for_quotation()
|
||||||
|
@ -84,9 +84,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
|||||||
cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
|
cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
|
||||||
},
|
},
|
||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
|
||||||
frappe.click_check('No Quote');
|
|
||||||
},
|
|
||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
() => {
|
||||||
cur_frm.cur_grid.toggle_view();
|
cur_frm.cur_grid.toggle_view();
|
||||||
@ -125,7 +122,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
|||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
() => {
|
||||||
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Received");
|
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Received");
|
||||||
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[0].doc.no_quote == 1);
|
|
||||||
},
|
},
|
||||||
() => done()
|
() => done()
|
||||||
]);
|
]);
|
||||||
|
@ -27,10 +27,11 @@
|
|||||||
"stock_qty",
|
"stock_qty",
|
||||||
"warehouse_and_reference",
|
"warehouse_and_reference",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"project_name",
|
|
||||||
"col_break4",
|
"col_break4",
|
||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
|
"section_break_24",
|
||||||
|
"project_name",
|
||||||
"section_break_23",
|
"section_break_23",
|
||||||
"page_break"
|
"page_break"
|
||||||
],
|
],
|
||||||
@ -161,7 +162,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "project_name",
|
"fieldname": "project_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project Name",
|
"label": "Project",
|
||||||
"options": "Project",
|
"options": "Project",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@ -249,11 +250,18 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_24",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-12 19:10:36.333441",
|
"modified": "2020-09-24 17:26:46.276934",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation Item",
|
"name": "Request for Quotation Item",
|
||||||
|
@ -9,19 +9,19 @@
|
|||||||
"email_sent",
|
"email_sent",
|
||||||
"supplier",
|
"supplier",
|
||||||
"contact",
|
"contact",
|
||||||
"no_quote",
|
|
||||||
"quote_status",
|
"quote_status",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"supplier_name",
|
"supplier_name",
|
||||||
"email_id",
|
"email_id"
|
||||||
"download_pdf"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
|
"columns": 2,
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "send_email",
|
"fieldname": "send_email",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Send Email"
|
"label": "Send Email"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 4,
|
"columns": 2,
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"columns": 3,
|
"columns": 2,
|
||||||
"fieldname": "contact",
|
"fieldname": "contact",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -55,19 +55,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"default": "0",
|
"depends_on": "eval:doc.docstatus >= 1",
|
||||||
"depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'",
|
|
||||||
"fieldname": "no_quote",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "No Quote"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote",
|
|
||||||
"fieldname": "quote_status",
|
"fieldname": "quote_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Quote Status",
|
"label": "Quote Status",
|
||||||
"options": "Pending\nReceived\nNo Quote",
|
"options": "Pending\nReceived",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -90,17 +82,12 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Email Id",
|
"label": "Email Id",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"fieldname": "download_pdf",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Download PDF"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-28 19:31:11.855588",
|
"modified": "2020-10-16 12:23:41.769820",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation Supplier",
|
"name": "Request for Quotation Supplier",
|
||||||
|
@ -91,12 +91,7 @@ class SupplierQuotation(BuyingController):
|
|||||||
for my_item in self.items) if include_me else 0
|
for my_item in self.items) if include_me else 0
|
||||||
if (sqi_count.count + self_count) == 0:
|
if (sqi_count.count + self_count) == 0:
|
||||||
quote_status = _('Pending')
|
quote_status = _('Pending')
|
||||||
if quote_status == _('Received') and doc_sup.quote_status == _('No Quote'):
|
|
||||||
frappe.msgprint(_("{0} indicates that {1} will not provide a quotation, but all items \
|
|
||||||
have been quoted. Updating the RFQ quote status.").format(doc.name, self.supplier))
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'no_quote', 0)
|
|
||||||
elif doc_sup.quote_status != _('No Quote'):
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
|
@ -49,6 +49,22 @@ data = {
|
|||||||
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
|
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
|
||||||
'insert_after': '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'
|
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
{% if not jQuery.isEmptyObject(data) %}
|
||||||
|
<h5 style="margin-top: 20px;"> {{ __("Balance Details") }} </h5>
|
||||||
|
<table class="table table-bordered small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 20%">{{ __("Account Type") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Current Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Available Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Reserved Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Uncleared Balance") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for(const [key, value] of Object.entries(data)) { %}
|
||||||
|
<tr>
|
||||||
|
<td> {%= key %} </td>
|
||||||
|
<td class="text-right"> {%= value["current_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["available_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["reserved_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["uncleared_balance"] %} </td>
|
||||||
|
</tr>
|
||||||
|
{% } %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p style="margin-top: 30px;"> Account Balance Information Not Available. </p>
|
||||||
|
{% endif %}
|
@ -0,0 +1,118 @@
|
|||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
class MpesaConnector():
|
||||||
|
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
|
||||||
|
live_url="https://safaricom.co.ke"):
|
||||||
|
"""Setup configuration for Mpesa connector and generate new access token."""
|
||||||
|
self.env = env
|
||||||
|
self.app_key = app_key
|
||||||
|
self.app_secret = app_secret
|
||||||
|
if env == "sandbox":
|
||||||
|
self.base_url = sandbox_url
|
||||||
|
else:
|
||||||
|
self.base_url = live_url
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
"""
|
||||||
|
This method is used to fetch the access token required by Mpesa.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
|
||||||
|
"""
|
||||||
|
authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
|
||||||
|
authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
|
||||||
|
r = requests.get(
|
||||||
|
authenticate_url,
|
||||||
|
auth=HTTPBasicAuth(self.app_key, self.app_secret)
|
||||||
|
)
|
||||||
|
self.authentication_token = r.json()['access_token']
|
||||||
|
return r.json()['access_token']
|
||||||
|
|
||||||
|
def get_balance(self, initiator=None, security_credential=None, party_a=None, identifier_type=None,
|
||||||
|
remarks=None, queue_timeout_url=None,result_url=None):
|
||||||
|
"""
|
||||||
|
This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initiator (str): Username used to authenticate the transaction.
|
||||||
|
security_credential (str): Generate from developer portal.
|
||||||
|
command_id (str): AccountBalance.
|
||||||
|
party_a (int): Till number being queried.
|
||||||
|
identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
|
||||||
|
remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
|
||||||
|
queue_timeout_url (str): The url that handles information of timed out transactions.
|
||||||
|
result_url (str): The url that receives results from M-Pesa api call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OriginatorConverstionID (str): The unique request ID for tracking a transaction.
|
||||||
|
ConversationID (str): The unique request ID returned by mpesa for each request made
|
||||||
|
ResponseDescription (str): Response Description message
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Initiator": initiator,
|
||||||
|
"SecurityCredential": security_credential,
|
||||||
|
"CommandID": "AccountBalance",
|
||||||
|
"PartyA": party_a,
|
||||||
|
"IdentifierType": identifier_type,
|
||||||
|
"Remarks": remarks,
|
||||||
|
"QueueTimeOutURL": queue_timeout_url,
|
||||||
|
"ResultURL": result_url
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def stk_push(self, business_shortcode=None, passcode=None, amount=None, callback_url=None, reference_code=None,
|
||||||
|
phone_number=None, description=None):
|
||||||
|
"""
|
||||||
|
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
business_shortcode (int): The short code of the organization.
|
||||||
|
passcode (str): Get from developer portal
|
||||||
|
amount (int): The amount being transacted
|
||||||
|
callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
|
||||||
|
reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
|
||||||
|
phone_number(int): The Mobile Number to receive the STK Pin Prompt.
|
||||||
|
description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
|
||||||
|
|
||||||
|
Success Response:
|
||||||
|
CustomerMessage(str): Messages that customers can understand.
|
||||||
|
CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
|
||||||
|
ResponseDescription(str): Describes Success or failure
|
||||||
|
MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
|
||||||
|
ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
|
||||||
|
|
||||||
|
Error Reponse:
|
||||||
|
requestId(str): This is a unique requestID for the payment request
|
||||||
|
errorCode(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
errorMessage(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
|
||||||
|
password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
|
||||||
|
encoded = base64.b64encode(bytes(password, encoding='utf8'))
|
||||||
|
payload = {
|
||||||
|
"BusinessShortCode": business_shortcode,
|
||||||
|
"Password": encoded.decode("utf-8"),
|
||||||
|
"Timestamp": time,
|
||||||
|
"TransactionType": "CustomerPayBillOnline",
|
||||||
|
"Amount": amount,
|
||||||
|
"PartyA": int(phone_number),
|
||||||
|
"PartyB": business_shortcode,
|
||||||
|
"PhoneNumber": int(phone_number),
|
||||||
|
"CallBackURL": callback_url,
|
||||||
|
"AccountReference": reference_code,
|
||||||
|
"TransactionDesc": description
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
@ -0,0 +1,53 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
def create_custom_pos_fields():
|
||||||
|
"""Create custom fields corresponding to POS Settings and POS Invoice."""
|
||||||
|
pos_field = {
|
||||||
|
"POS Invoice": [
|
||||||
|
{
|
||||||
|
"fieldname": "request_for_payment",
|
||||||
|
"label": "Request for Payment",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"hidden": 1,
|
||||||
|
"insert_after": "contact_email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mpesa_receipt_number",
|
||||||
|
"label": "Mpesa Receipt Number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"read_only": 1,
|
||||||
|
"insert_after": "company"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
|
||||||
|
create_custom_fields(pos_field)
|
||||||
|
|
||||||
|
record_dict = [{
|
||||||
|
"doctype": "POS Field",
|
||||||
|
"fieldname": "contact_mobile",
|
||||||
|
"label": "Mobile No",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"options": "Phone",
|
||||||
|
"parenttype": "POS Settings",
|
||||||
|
"parent": "POS Settings",
|
||||||
|
"parentfield": "invoice_fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "POS Field",
|
||||||
|
"fieldname": "request_for_payment",
|
||||||
|
"label": "Request for Payment",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"parenttype": "POS Settings",
|
||||||
|
"parent": "POS Settings",
|
||||||
|
"parentfield": "invoice_fields"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
create_pos_settings(record_dict)
|
||||||
|
|
||||||
|
def create_pos_settings(record_dict):
|
||||||
|
for record in record_dict:
|
||||||
|
if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}):
|
||||||
|
continue
|
||||||
|
frappe.get_doc(record).insert()
|
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Mpesa Settings', {
|
||||||
|
onload_post_render: function(frm) {
|
||||||
|
frm.events.setup_account_balance_html(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frappe.realtime.on("refresh_mpesa_dashboard", function(){
|
||||||
|
frm.reload_doc();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_account_balance: function(frm) {
|
||||||
|
if (!frm.initiator_name && !frm.security_credentials) {
|
||||||
|
frappe.throw(__("Please set the initiator name and the security credential"));
|
||||||
|
}
|
||||||
|
frappe.call({
|
||||||
|
method: "get_account_balance_info",
|
||||||
|
doc: frm.doc
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setup_account_balance_html: function(frm) {
|
||||||
|
if (!frm.doc.account_balance) return;
|
||||||
|
$("div").remove(".form-dashboard-section.custom");
|
||||||
|
frm.dashboard.add_section(
|
||||||
|
frappe.render_template('account_balance', {
|
||||||
|
data: JSON.parse(frm.doc.account_balance)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
frm.dashboard.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "field:payment_gateway_name",
|
||||||
|
"creation": "2020-09-10 13:21:27.398088",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"payment_gateway_name",
|
||||||
|
"consumer_key",
|
||||||
|
"consumer_secret",
|
||||||
|
"initiator_name",
|
||||||
|
"till_number",
|
||||||
|
"sandbox",
|
||||||
|
"column_break_4",
|
||||||
|
"online_passkey",
|
||||||
|
"security_credential",
|
||||||
|
"get_account_balance",
|
||||||
|
"account_balance"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "payment_gateway_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Payment Gateway Name",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "consumer_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Consumer Key",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "consumer_secret",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Consumer Secret",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "till_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Till Number",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "sandbox",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Sandbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "online_passkey",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"label": " Online PassKey",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "initiator_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Initiator Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "security_credential",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Security Credential"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_balance",
|
||||||
|
"fieldtype": "Long Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Account Balance",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_account_balance",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Account Balance"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-25 20:21:38.215494",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "ERPNext Integrations",
|
||||||
|
"name": "Mpesa Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
@ -0,0 +1,208 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from json import loads, dumps
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import call_hook_method, fmt_money
|
||||||
|
from frappe.integrations.utils import create_request_log, create_payment_gateway
|
||||||
|
from frappe.utils import get_request_site_address
|
||||||
|
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import create_custom_pos_fields
|
||||||
|
|
||||||
|
class MpesaSettings(Document):
|
||||||
|
supported_currencies = ["KES"]
|
||||||
|
|
||||||
|
def validate_transaction_currency(self, currency):
|
||||||
|
if currency not in self.supported_currencies:
|
||||||
|
frappe.throw(_("Please select another payment method. Mpesa does not support transactions in currency '{0}'").format(currency))
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
create_custom_pos_fields()
|
||||||
|
create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name)
|
||||||
|
call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone")
|
||||||
|
|
||||||
|
# required to fetch the bank account details from the payment gateway account
|
||||||
|
frappe.db.commit()
|
||||||
|
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
|
||||||
|
|
||||||
|
def request_for_payment(self, **kwargs):
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
|
||||||
|
response = frappe._dict(get_payment_request_response_payload())
|
||||||
|
else:
|
||||||
|
response = frappe._dict(generate_stk_push(**kwargs))
|
||||||
|
|
||||||
|
self.handle_api_response("CheckoutRequestID", kwargs, response)
|
||||||
|
|
||||||
|
def get_account_balance_info(self):
|
||||||
|
payload = dict(
|
||||||
|
reference_doctype="Mpesa Settings",
|
||||||
|
reference_docname=self.name,
|
||||||
|
doc_details=vars(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_test_account_balance_response
|
||||||
|
response = frappe._dict(get_test_account_balance_response())
|
||||||
|
else:
|
||||||
|
response = frappe._dict(get_account_balance(payload))
|
||||||
|
|
||||||
|
self.handle_api_response("ConversationID", payload, response)
|
||||||
|
|
||||||
|
def handle_api_response(self, global_id, request_dict, response):
|
||||||
|
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
|
||||||
|
# check error response
|
||||||
|
if getattr(response, "requestId"):
|
||||||
|
req_name = getattr(response, "requestId")
|
||||||
|
error = response
|
||||||
|
else:
|
||||||
|
# global checkout id used as request name
|
||||||
|
req_name = getattr(response, global_id)
|
||||||
|
error = None
|
||||||
|
|
||||||
|
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
||||||
|
|
||||||
|
def generate_stk_push(**kwargs):
|
||||||
|
"""Generate stk push by making a API call to the stk push API."""
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
try:
|
||||||
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
||||||
|
|
||||||
|
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
|
||||||
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
|
|
||||||
|
connector = MpesaConnector(env=env,
|
||||||
|
app_key=mpesa_settings.consumer_key,
|
||||||
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
mobile_number = sanitize_mobile_number(args.sender)
|
||||||
|
|
||||||
|
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
||||||
|
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
||||||
|
callback_url=callback_url, reference_code=mpesa_settings.till_number,
|
||||||
|
phone_number=mobile_number, description="POS Payment")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(title=_("Mpesa Express Transaction Error"))
|
||||||
|
frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"))
|
||||||
|
|
||||||
|
def sanitize_mobile_number(number):
|
||||||
|
"""Add country code and strip leading zeroes from the phone number."""
|
||||||
|
return "254" + str(number).lstrip("0")
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def verify_transaction(**kwargs):
|
||||||
|
"""Verify the transaction result received via callback from stk."""
|
||||||
|
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||||
|
|
||||||
|
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||||||
|
request = frappe.get_doc("Integration Request", checkout_id)
|
||||||
|
transaction_data = frappe._dict(loads(request.data))
|
||||||
|
|
||||||
|
if transaction_response['ResultCode'] == 0:
|
||||||
|
if request.reference_doctype and request.reference_docname:
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc(request.reference_doctype,
|
||||||
|
request.reference_docname)
|
||||||
|
doc.run_method("on_payment_authorized", 'Completed')
|
||||||
|
|
||||||
|
item_response = transaction_response["CallbackMetadata"]["Item"]
|
||||||
|
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
||||||
|
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
|
||||||
|
request.handle_success(transaction_response)
|
||||||
|
except Exception:
|
||||||
|
request.handle_failure(transaction_response)
|
||||||
|
frappe.log_error(frappe.get_traceback())
|
||||||
|
|
||||||
|
else:
|
||||||
|
request.handle_failure(transaction_response)
|
||||||
|
|
||||||
|
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
|
||||||
|
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
|
||||||
|
|
||||||
|
def get_account_balance(request_payload):
|
||||||
|
"""Call account balance API to send the request to the Mpesa Servers."""
|
||||||
|
try:
|
||||||
|
mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
|
||||||
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
|
connector = MpesaConnector(env=env,
|
||||||
|
app_key=mpesa_settings.consumer_key,
|
||||||
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
||||||
|
|
||||||
|
response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url)
|
||||||
|
return response
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(title=_("Account Balance Processing Error"))
|
||||||
|
frappe.throw(title=_("Error"), message=_("Please check your configuration and try again"))
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def process_balance_info(**kwargs):
|
||||||
|
"""Process and store account balance information received via callback from the account balance API call."""
|
||||||
|
account_balance_response = frappe._dict(kwargs["Result"])
|
||||||
|
|
||||||
|
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
||||||
|
request = frappe.get_doc("Integration Request", conversation_id)
|
||||||
|
|
||||||
|
if request.status == "Completed":
|
||||||
|
return
|
||||||
|
|
||||||
|
transaction_data = frappe._dict(loads(request.data))
|
||||||
|
|
||||||
|
if account_balance_response["ResultCode"] == 0:
|
||||||
|
try:
|
||||||
|
result_params = account_balance_response["ResultParameters"]["ResultParameter"]
|
||||||
|
|
||||||
|
balance_info = fetch_param_value(result_params, "AccountBalance", "Key")
|
||||||
|
balance_info = format_string_to_json(balance_info)
|
||||||
|
|
||||||
|
ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
|
||||||
|
ref_doc.db_set("account_balance", balance_info)
|
||||||
|
|
||||||
|
request.handle_success(account_balance_response)
|
||||||
|
frappe.publish_realtime("refresh_mpesa_dashboard")
|
||||||
|
except Exception:
|
||||||
|
request.handle_failure(account_balance_response)
|
||||||
|
frappe.log_error(title=_("Mpesa Account Balance Processing Error"), message=account_balance_response)
|
||||||
|
else:
|
||||||
|
request.handle_failure(account_balance_response)
|
||||||
|
|
||||||
|
def format_string_to_json(balance_info):
|
||||||
|
"""
|
||||||
|
Format string to json.
|
||||||
|
|
||||||
|
e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00'''
|
||||||
|
=> {'Working Account': {'current_balance': '481000.00',
|
||||||
|
'available_balance': '481000.00',
|
||||||
|
'reserved_balance': '0.00',
|
||||||
|
'uncleared_balance': '0.00'}}
|
||||||
|
"""
|
||||||
|
balance_dict = frappe._dict()
|
||||||
|
for account_info in balance_info.split("&"):
|
||||||
|
account_info = account_info.split('|')
|
||||||
|
balance_dict[account_info[0]] = dict(
|
||||||
|
current_balance=fmt_money(account_info[2], currency="KES"),
|
||||||
|
available_balance=fmt_money(account_info[3], currency="KES"),
|
||||||
|
reserved_balance=fmt_money(account_info[4], currency="KES"),
|
||||||
|
uncleared_balance=fmt_money(account_info[5], currency="KES")
|
||||||
|
)
|
||||||
|
return dumps(balance_dict)
|
||||||
|
|
||||||
|
def fetch_param_value(response, key, key_field):
|
||||||
|
"""Fetch the specified key from list of dictionary. Key is identified via the key field."""
|
||||||
|
for param in response:
|
||||||
|
if param[key_field] == key:
|
||||||
|
return param["Value"]
|
@ -0,0 +1,240 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from json import dumps
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||||
|
|
||||||
|
class TestMpesaSettings(unittest.TestCase):
|
||||||
|
def test_creation_of_payment_gateway(self):
|
||||||
|
create_mpesa_settings(payment_gateway_name="_Test")
|
||||||
|
|
||||||
|
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
|
||||||
|
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
||||||
|
self.assertTrue(mode_of_payment.name)
|
||||||
|
self.assertEquals(mode_of_payment.type, "Phone")
|
||||||
|
|
||||||
|
def test_processing_of_account_balance(self):
|
||||||
|
mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance")
|
||||||
|
mpesa_doc.get_account_balance_info()
|
||||||
|
|
||||||
|
callback_response = get_account_balance_callback_payload()
|
||||||
|
process_balance_info(**callback_response)
|
||||||
|
integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315")
|
||||||
|
|
||||||
|
# test integration request creation and successful update of the status on receiving callback response
|
||||||
|
self.assertTrue(integration_request)
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
# test formatting of account balance received as string to json with appropriate currency symbol
|
||||||
|
mpesa_doc.reload()
|
||||||
|
self.assertEquals(mpesa_doc.account_balance, dumps({
|
||||||
|
"Working Account": {
|
||||||
|
"current_balance": "Sh 481,000.00",
|
||||||
|
"available_balance": "Sh 481,000.00",
|
||||||
|
"reserved_balance": "Sh 0.00",
|
||||||
|
"uncleared_balance": "Sh 0.00"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
def test_processing_of_callback_payload(self):
|
||||||
|
create_mpesa_settings(payment_gateway_name="Payment")
|
||||||
|
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||||
|
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||||
|
|
||||||
|
pos_invoice = create_pos_invoice(do_not_submit=1)
|
||||||
|
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500})
|
||||||
|
pos_invoice.contact_mobile = "093456543894"
|
||||||
|
pos_invoice.currency = "KES"
|
||||||
|
pos_invoice.save()
|
||||||
|
|
||||||
|
pr = pos_invoice.create_payment_request()
|
||||||
|
# test payment request creation
|
||||||
|
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
|
||||||
|
|
||||||
|
callback_response = get_payment_callback_payload()
|
||||||
|
verify_transaction(**callback_response)
|
||||||
|
# test creation of integration request
|
||||||
|
integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972")
|
||||||
|
|
||||||
|
# test integration request creation and successful update of the status on receiving callback response
|
||||||
|
self.assertTrue(integration_request)
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
pos_invoice.reload()
|
||||||
|
integration_request.reload()
|
||||||
|
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
def create_mpesa_settings(payment_gateway_name="Express"):
|
||||||
|
if frappe.db.exists("Mpesa Settings", payment_gateway_name):
|
||||||
|
return frappe.get_doc("Mpesa Settings", payment_gateway_name)
|
||||||
|
|
||||||
|
doc = frappe.get_doc(dict( #nosec
|
||||||
|
doctype="Mpesa Settings",
|
||||||
|
payment_gateway_name=payment_gateway_name,
|
||||||
|
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
||||||
|
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
||||||
|
online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd",
|
||||||
|
till_number="174379"
|
||||||
|
))
|
||||||
|
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def get_test_account_balance_response():
|
||||||
|
"""Response received after calling the account balance API."""
|
||||||
|
return {
|
||||||
|
"ResultType":0,
|
||||||
|
"ResultCode":0,
|
||||||
|
"ResultDesc":"The service request has been accepted successfully.",
|
||||||
|
"OriginatorConversationID":"10816-694520-2",
|
||||||
|
"ConversationID":"AG_20200927_00007cdb1f9fb6494315",
|
||||||
|
"TransactionID":"LGR0000000",
|
||||||
|
"ResultParameters":{
|
||||||
|
"ResultParameter":[
|
||||||
|
{
|
||||||
|
"Key":"ReceiptNo",
|
||||||
|
"Value":"LGR919G2AV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Conversation ID",
|
||||||
|
"Value":"AG_20170727_00004492b1b6d0078fbe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"FinalisedTime",
|
||||||
|
"Value":20170727101415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Amount",
|
||||||
|
"Value":10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"TransactionStatus",
|
||||||
|
"Value":"Completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"ReasonType",
|
||||||
|
"Value":"Salary Payment via API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"TransactionReason"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitPartyCharges",
|
||||||
|
"Value":"Fee For B2C Payment|KES|33.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitAccountType",
|
||||||
|
"Value":"Utility Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"InitiatedTime",
|
||||||
|
"Value":20170727101415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Originator Conversation ID",
|
||||||
|
"Value":"19455-773836-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"CreditPartyName",
|
||||||
|
"Value":"254708374149 - John Doe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitPartyName",
|
||||||
|
"Value":"600134 - Safaricom157"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ReferenceData":{
|
||||||
|
"ReferenceItem":{
|
||||||
|
"Key":"Occasion",
|
||||||
|
"Value":"aaaa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_payment_request_response_payload():
|
||||||
|
"""Response received after successfully calling the stk push process request API."""
|
||||||
|
return {
|
||||||
|
"MerchantRequestID": "8071-27184008-1",
|
||||||
|
"CheckoutRequestID": "ws_CO_061020201133231972",
|
||||||
|
"ResultCode": 0,
|
||||||
|
"ResultDesc": "The service request is processed successfully.",
|
||||||
|
"CallbackMetadata": {
|
||||||
|
"Item": [
|
||||||
|
{ "Name": "Amount", "Value": 500.0 },
|
||||||
|
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
|
||||||
|
{ "Name": "TransactionDate", "Value": 20201006113336 },
|
||||||
|
{ "Name": "PhoneNumber", "Value": 254723575670 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_callback_payload():
|
||||||
|
"""Response received from the server as callback after calling the stkpush process request API."""
|
||||||
|
return {
|
||||||
|
"Body":{
|
||||||
|
"stkCallback":{
|
||||||
|
"MerchantRequestID":"19465-780693-1",
|
||||||
|
"CheckoutRequestID":"ws_CO_061020201133231972",
|
||||||
|
"ResultCode":0,
|
||||||
|
"ResultDesc":"The service request is processed successfully.",
|
||||||
|
"CallbackMetadata":{
|
||||||
|
"Item":[
|
||||||
|
{
|
||||||
|
"Name":"Amount",
|
||||||
|
"Value":500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"MpesaReceiptNumber",
|
||||||
|
"Value":"LGR7OWQX0R"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"Balance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"TransactionDate",
|
||||||
|
"Value":20170727154800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"PhoneNumber",
|
||||||
|
"Value":254721566839
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_account_balance_callback_payload():
|
||||||
|
"""Response received from the server as callback after calling the account balance API."""
|
||||||
|
return {
|
||||||
|
"Result":{
|
||||||
|
"ResultType": 0,
|
||||||
|
"ResultCode": 0,
|
||||||
|
"ResultDesc": "The service request is processed successfully.",
|
||||||
|
"OriginatorConversationID": "16470-170099139-1",
|
||||||
|
"ConversationID": "AG_20200927_00007cdb1f9fb6494315",
|
||||||
|
"TransactionID": "OIR0000000",
|
||||||
|
"ResultParameters": {
|
||||||
|
"ResultParameter": [
|
||||||
|
{
|
||||||
|
"Key": "AccountBalance",
|
||||||
|
"Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"
|
||||||
|
},
|
||||||
|
{ "Key": "BOCompletedTime", "Value": 20200927234123 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ReferenceData": {
|
||||||
|
"ReferenceItem": {
|
||||||
|
"Key": "QueueTimeoutURL",
|
||||||
|
"Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
import base64, hashlib, hmac
|
import base64, hashlib, hmac
|
||||||
from six.moves.urllib.parse import urlparse
|
from six.moves.urllib.parse import urlparse
|
||||||
|
from erpnext import get_default_company
|
||||||
|
|
||||||
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
||||||
def innerfn(fn):
|
def innerfn(fn):
|
||||||
@ -41,3 +42,22 @@ def get_webhook_address(connector_name, method, exclude_uri=False):
|
|||||||
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
|
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
|
||||||
|
|
||||||
return server_url
|
return server_url
|
||||||
|
|
||||||
|
def create_mode_of_payment(gateway, payment_type="General"):
|
||||||
|
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
|
||||||
|
"payment_gateway": gateway
|
||||||
|
}, ['payment_account'])
|
||||||
|
|
||||||
|
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
|
||||||
|
mode_of_payment = frappe.get_doc({
|
||||||
|
"doctype": "Mode of Payment",
|
||||||
|
"mode_of_payment": gateway,
|
||||||
|
"enabled": 1,
|
||||||
|
"type": payment_type,
|
||||||
|
"accounts": [{
|
||||||
|
"doctype": "Mode of Payment Account",
|
||||||
|
"company": get_default_company(),
|
||||||
|
"default_account": payment_gateway_account
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
mode_of_payment.insert(ignore_permissions=True)
|
@ -43,7 +43,8 @@
|
|||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Dosage",
|
"label": "Dosage",
|
||||||
"options": "Prescription Dosage"
|
"options": "Prescription Dosage",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "period",
|
"fieldname": "period",
|
||||||
@ -51,14 +52,16 @@
|
|||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Period",
|
"label": "Period",
|
||||||
"options": "Prescription Duration"
|
"options": "Prescription Duration",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "dosage_form",
|
"fieldname": "dosage_form",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"label": "Dosage Form",
|
"label": "Dosage Form",
|
||||||
"options": "Dosage Form"
|
"options": "Dosage Form",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_7",
|
"fieldname": "column_break_7",
|
||||||
@ -72,7 +75,7 @@
|
|||||||
"label": "Comment"
|
"label": "Comment"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "use_interval",
|
"depends_on": "usage_interval",
|
||||||
"fieldname": "interval",
|
"fieldname": "interval",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -80,6 +83,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"depends_on": "usage_interval",
|
||||||
"fieldname": "update_schedule",
|
"fieldname": "update_schedule",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@ -99,12 +103,13 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "usage_interval",
|
"fieldname": "usage_interval",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Dosage by Time Interval"
|
"label": "Dosage by Time Interval"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-02-26 17:02:42.741338",
|
"modified": "2020-09-30 23:32:09.495288",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Healthcare",
|
"module": "Healthcare",
|
||||||
"name": "Drug Prescription",
|
"name": "Drug Prescription",
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -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
|
||||||
|
}
|
@ -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
|
@ -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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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()
|
@ -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
|
||||||
|
}
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
@ -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
|
||||||
|
}
|
@ -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
|
@ -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"];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
@ -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
|
@ -83,6 +83,7 @@ def get_healthcare_service_unit():
|
|||||||
if not service_unit:
|
if not service_unit:
|
||||||
service_unit = frappe.new_doc("Healthcare Service Unit")
|
service_unit = frappe.new_doc("Healthcare Service Unit")
|
||||||
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
|
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
|
||||||
|
service_unit.company = "_Test Company"
|
||||||
service_unit.service_unit_type = get_service_unit_type()
|
service_unit.service_unit_type = get_service_unit_type()
|
||||||
service_unit.inpatient_occupancy = 1
|
service_unit.inpatient_occupancy = 1
|
||||||
service_unit.occupancy_status = "Vacant"
|
service_unit.occupancy_status = "Vacant"
|
||||||
|
@ -58,6 +58,14 @@ frappe.ui.form.on('Patient Encounter', {
|
|||||||
create_procedure(frm);
|
create_procedure(frm);
|
||||||
},'Create');
|
},'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() {
|
frm.set_query('patient', function() {
|
||||||
|
@ -6,8 +6,9 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr, getdate, add_days
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
class PatientEncounter(Document):
|
class PatientEncounter(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -22,20 +23,69 @@ class PatientEncounter(Document):
|
|||||||
insert_encounter_to_medical_record(self)
|
insert_encounter_to_medical_record(self)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
update_encounter_medical_record(self)
|
if self.therapies:
|
||||||
|
create_therapy_plan(self)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
if self.appointment:
|
if self.appointment:
|
||||||
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
|
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
|
||||||
delete_medical_record(self)
|
|
||||||
|
|
||||||
def on_submit(self):
|
if self.inpatient_record and self.drug_prescription:
|
||||||
create_therapy_plan(self)
|
delete_ip_medication_order(self)
|
||||||
|
|
||||||
|
delete_medical_record(self)
|
||||||
|
|
||||||
def set_title(self):
|
def set_title(self):
|
||||||
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
|
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
|
||||||
self.practitioner_name or self.practitioner)[:100]
|
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):
|
def create_therapy_plan(encounter):
|
||||||
if len(encounter.therapies):
|
if len(encounter.therapies):
|
||||||
doc = frappe.new_doc('Therapy Plan')
|
doc = frappe.new_doc('Therapy Plan')
|
||||||
@ -51,6 +101,7 @@ def create_therapy_plan(encounter):
|
|||||||
encounter.db_set('therapy_plan', doc.name)
|
encounter.db_set('therapy_plan', doc.name)
|
||||||
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
|
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
|
||||||
|
|
||||||
|
|
||||||
def insert_encounter_to_medical_record(doc):
|
def insert_encounter_to_medical_record(doc):
|
||||||
subject = set_subject_field(doc)
|
subject = set_subject_field(doc)
|
||||||
medical_record = frappe.new_doc('Patient Medical Record')
|
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.reference_owner = doc.owner
|
||||||
medical_record.save(ignore_permissions=True)
|
medical_record.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
def update_encounter_medical_record(encounter):
|
def update_encounter_medical_record(encounter):
|
||||||
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
|
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
|
||||||
|
|
||||||
@ -72,8 +124,17 @@ def update_encounter_medical_record(encounter):
|
|||||||
else:
|
else:
|
||||||
insert_encounter_to_medical_record(encounter)
|
insert_encounter_to_medical_record(encounter)
|
||||||
|
|
||||||
|
|
||||||
def delete_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):
|
def set_subject_field(encounter):
|
||||||
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
|
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
|
||||||
|
@ -5,12 +5,18 @@ def get_data():
|
|||||||
return {
|
return {
|
||||||
'fieldname': 'encounter',
|
'fieldname': 'encounter',
|
||||||
'non_standard_fieldnames': {
|
'non_standard_fieldnames': {
|
||||||
'Patient Medical Record': 'reference_name'
|
'Patient Medical Record': 'reference_name',
|
||||||
|
'Inpatient Medication Order': 'patient_encounter'
|
||||||
},
|
},
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
'label': _('Records'),
|
'label': _('Records'),
|
||||||
'items': ['Vital Signs', 'Patient Medical Record']
|
'items': ['Vital Signs', 'Patient Medical Record']
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
'label': _('Orders'),
|
||||||
|
'items': ['Inpatient Medication Order']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'disable_create_buttons': ['Inpatient Medication Order']
|
||||||
}
|
}
|
||||||
|
@ -282,7 +282,8 @@ doc_events = {
|
|||||||
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
|
# 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.
|
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
|
||||||
auto_cancel_exempted_doctypes= [
|
auto_cancel_exempted_doctypes= [
|
||||||
"Payment Entry"
|
"Payment Entry",
|
||||||
|
"Inpatient Medication Entry"
|
||||||
]
|
]
|
||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
|
@ -56,7 +56,7 @@ class Employee(NestedSet):
|
|||||||
if existing_user_id:
|
if existing_user_id:
|
||||||
remove_user_permission(
|
remove_user_permission(
|
||||||
"Employee", self.name, existing_user_id)
|
"Employee", self.name, existing_user_id)
|
||||||
|
|
||||||
def after_rename(self, old, new, merge):
|
def after_rename(self, old, new, merge):
|
||||||
self.db_set("employee", new)
|
self.db_set("employee", new)
|
||||||
|
|
||||||
@ -181,8 +181,11 @@ class Employee(NestedSet):
|
|||||||
)
|
)
|
||||||
if reports_to:
|
if reports_to:
|
||||||
link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to]
|
link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to]
|
||||||
throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ")
|
message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name))
|
||||||
+ ', '.join(link_to_employees), EmployeeLeftValidationError)
|
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
|
||||||
|
message += "</li></ul><br>"
|
||||||
|
message += _("Please make sure the employees above report to another Active employee.")
|
||||||
|
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee"))
|
||||||
if not self.relieving_date:
|
if not self.relieving_date:
|
||||||
throw(_("Please enter relieving date."))
|
throw(_("Please enter relieving date."))
|
||||||
|
|
||||||
@ -215,7 +218,7 @@ class Employee(NestedSet):
|
|||||||
|
|
||||||
def validate_preferred_email(self):
|
def validate_preferred_email(self):
|
||||||
if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)):
|
if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)):
|
||||||
frappe.msgprint(_("Please enter " + self.prefered_contact_email))
|
frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email))
|
||||||
|
|
||||||
def validate_onboarding_process(self):
|
def validate_onboarding_process(self):
|
||||||
employee_onboarding = frappe.get_all("Employee Onboarding",
|
employee_onboarding = frappe.get_all("Employee Onboarding",
|
||||||
@ -417,9 +420,9 @@ def get_employee_emails(employee_list):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
|
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
|
||||||
|
|
||||||
filters = []
|
filters = [['status', '!=', 'Left']]
|
||||||
if company and company != 'All Companies':
|
if company and company != 'All Companies':
|
||||||
filters = [['company', '=', company]]
|
filters.append(['company', '=', company])
|
||||||
|
|
||||||
fields = ['name as value', 'employee_name as title']
|
fields = ['name as value', 'employee_name as title']
|
||||||
|
|
||||||
|
@ -28,144 +28,110 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "employee_settings",
|
"fieldname": "employee_settings",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Employee Settings",
|
"label": "Employee Settings"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enter retirement age in years",
|
"description": "Enter retirement age in years",
|
||||||
"fieldname": "retirement_age",
|
"fieldname": "retirement_age",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Retirement Age",
|
"label": "Retirement Age"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Naming Series",
|
"default": "Naming Series",
|
||||||
"description": "Employee record is created using selected field. ",
|
"description": "Employee records are created using the selected field",
|
||||||
"fieldname": "emp_created_by",
|
"fieldname": "emp_created_by",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Employee Records to be created by",
|
"label": "Employee Records to Be Created By",
|
||||||
"options": "Naming Series\nEmployee Number\nFull Name",
|
"options": "Naming Series\nEmployee Number\nFull Name"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Don't send Employee Birthday Reminders",
|
"description": "Don't send employee birthday reminders",
|
||||||
"fieldname": "stop_birthday_reminders",
|
"fieldname": "stop_birthday_reminders",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Stop Birthday Reminders",
|
"label": "Stop Birthday Reminders"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "expense_approver_mandatory_in_expense_claim",
|
"fieldname": "expense_approver_mandatory_in_expense_claim",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Expense Approver Mandatory In Expense Claim",
|
"label": "Expense Approver Mandatory In Expense Claim"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "leave_settings",
|
"fieldname": "leave_settings",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Leave Settings",
|
"label": "Leave Settings"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "leave_approval_notification_template",
|
"fieldname": "leave_approval_notification_template",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Leave Approval Notification Template",
|
"label": "Leave Approval Notification Template",
|
||||||
"options": "Email Template",
|
"options": "Email Template"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "leave_status_notification_template",
|
"fieldname": "leave_status_notification_template",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Leave Status Notification Template",
|
"label": "Leave Status Notification Template",
|
||||||
"options": "Email Template",
|
"options": "Email Template"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_18",
|
"fieldname": "column_break_18",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "leave_approver_mandatory_in_leave_application",
|
"fieldname": "leave_approver_mandatory_in_leave_application",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Leave Approver Mandatory In Leave Application",
|
"label": "Leave Approver Mandatory In Leave Application"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "show_leaves_of_all_department_members_in_calendar",
|
"fieldname": "show_leaves_of_all_department_members_in_calendar",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Leaves Of All Department Members In Calendar",
|
"label": "Show Leaves Of All Department Members In Calendar"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "hiring_settings",
|
"fieldname": "hiring_settings",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Hiring Settings",
|
"label": "Hiring Settings"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "check_vacancies",
|
"fieldname": "check_vacancies",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Vacancies On Job Offer Creation",
|
"label": "Check Vacancies On Job Offer Creation"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "auto_leave_encashment",
|
"fieldname": "auto_leave_encashment",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Leave Encashment",
|
"label": "Auto Leave Encashment"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "restrict_backdated_leave_application",
|
"fieldname": "restrict_backdated_leave_application",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Restrict Backdated Leave Application",
|
"label": "Restrict Backdated Leave Applications"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
|
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
|
||||||
"fieldname": "role_allowed_to_create_backdated_leave_application",
|
"fieldname": "role_allowed_to_create_backdated_leave_application",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Create Backdated Leave Application",
|
"label": "Role Allowed to Create Backdated Leave Application",
|
||||||
"options": "Role",
|
"options": "Role"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-04 15:15:09.865476",
|
"modified": "2020-10-13 11:49:46.168027",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"name": "HR Settings",
|
||||||
@ -183,4 +149,4 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC"
|
"sort_order": "ASC"
|
||||||
}
|
}
|
||||||
|
@ -55,10 +55,11 @@ class BOM(WebsiteGenerator):
|
|||||||
conflicting_bom = frappe.get_doc("BOM", name)
|
conflicting_bom = frappe.get_doc("BOM", name)
|
||||||
|
|
||||||
if conflicting_bom.item != self.item:
|
if conflicting_bom.item != self.item:
|
||||||
|
msg = (_("A BOM with name {0} already exists for item {1}.")
|
||||||
|
.format(frappe.bold(name), frappe.bold(conflicting_bom.item)))
|
||||||
|
|
||||||
frappe.throw(_("""A BOM with name {0} already exists for item {1}.
|
frappe.throw(_("{0}{1} Did you rename the item? Please contact Administrator / Tech support")
|
||||||
<br> Did you rename the item? Please contact Administrator / Tech support
|
.format(msg, "<br>"))
|
||||||
""").format(frappe.bold(name), frappe.bold(conflicting_bom.item)))
|
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ class BOM(WebsiteGenerator):
|
|||||||
self.validate_uom_is_interger()
|
self.validate_uom_is_interger()
|
||||||
self.set_bom_material_details()
|
self.set_bom_material_details()
|
||||||
self.validate_materials()
|
self.validate_materials()
|
||||||
|
self.set_routing_operations()
|
||||||
self.validate_operations()
|
self.validate_operations()
|
||||||
self.calculate_cost()
|
self.calculate_cost()
|
||||||
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
||||||
@ -111,18 +113,13 @@ class BOM(WebsiteGenerator):
|
|||||||
def get_routing(self):
|
def get_routing(self):
|
||||||
if self.routing:
|
if self.routing:
|
||||||
self.set("operations", [])
|
self.set("operations", [])
|
||||||
for d in frappe.get_all("BOM Operation", fields = ["*"],
|
fields = ["sequence_id", "operation", "workstation", "description",
|
||||||
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="idx"):
|
"time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"]
|
||||||
child = self.append('operations', {
|
|
||||||
"operation": d.operation,
|
for row in frappe.get_all("BOM Operation", fields = fields,
|
||||||
"workstation": d.workstation,
|
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
|
||||||
"description": d.description,
|
child = self.append('operations', row)
|
||||||
"time_in_mins": d.time_in_mins,
|
child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2)
|
||||||
"batch_size": d.batch_size,
|
|
||||||
"operating_cost": d.operating_cost,
|
|
||||||
"idx": d.idx
|
|
||||||
})
|
|
||||||
child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2)
|
|
||||||
|
|
||||||
def set_bom_material_details(self):
|
def set_bom_material_details(self):
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
@ -571,6 +568,10 @@ class BOM(WebsiteGenerator):
|
|||||||
if act_pbom and act_pbom[0][0]:
|
if act_pbom and act_pbom[0][0]:
|
||||||
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
|
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
|
||||||
|
|
||||||
|
def set_routing_operations(self):
|
||||||
|
if self.routing and self.with_operations and not self.operations:
|
||||||
|
self.get_routing()
|
||||||
|
|
||||||
def validate_operations(self):
|
def validate_operations(self):
|
||||||
if self.with_operations and not self.get('operations'):
|
if self.with_operations and not self.get('operations'):
|
||||||
frappe.throw(_("Operations cannot be left blank"))
|
frappe.throw(_("Operations cannot be left blank"))
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2013-02-22 01:27:49",
|
"creation": "2013-02-22 01:27:49",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"sequence_id",
|
||||||
"operation",
|
"operation",
|
||||||
"workstation",
|
"workstation",
|
||||||
"description",
|
"description",
|
||||||
@ -106,11 +108,19 @@
|
|||||||
"fieldname": "batch_size",
|
"fieldname": "batch_size",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Batch Size"
|
"label": "Batch Size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.parenttype == \"Routing\"",
|
||||||
|
"fieldname": "sequence_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Sequence ID"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2020-06-16 17:01:11.128420",
|
"links": [],
|
||||||
|
"modified": "2020-10-13 18:14:10.018774",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"items",
|
"items",
|
||||||
"more_information",
|
"more_information",
|
||||||
"operation_id",
|
"operation_id",
|
||||||
|
"sequence_id",
|
||||||
"transferred_qty",
|
"transferred_qty",
|
||||||
"requested_qty",
|
"requested_qty",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
@ -297,10 +298,18 @@
|
|||||||
"fieldname": "operation_row_number",
|
"fieldname": "operation_row_number",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Operation Row Number"
|
"label": "Operation Row Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sequence_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Sequence Id",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"modified": "2020-08-24 15:21:21.398267",
|
"links": [],
|
||||||
|
"modified": "2020-10-14 12:58:25.327897",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
import datetime
|
import datetime
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
||||||
@ -16,12 +16,14 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
|
|||||||
class OverlapError(frappe.ValidationError): pass
|
class OverlapError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class OperationMismatchError(frappe.ValidationError): pass
|
class OperationMismatchError(frappe.ValidationError): pass
|
||||||
|
class OperationSequenceError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class JobCard(Document):
|
class JobCard(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_time_logs()
|
self.validate_time_logs()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_operation_id()
|
self.validate_operation_id()
|
||||||
|
self.validate_sequence_id()
|
||||||
|
|
||||||
def validate_time_logs(self):
|
def validate_time_logs(self):
|
||||||
self.total_completed_qty = 0.0
|
self.total_completed_qty = 0.0
|
||||||
@ -196,14 +198,14 @@ class JobCard(Document):
|
|||||||
def validate_job_card(self):
|
def validate_job_card(self):
|
||||||
if not self.time_logs:
|
if not self.time_logs:
|
||||||
frappe.throw(_("Time logs are required for {0} {1}")
|
frappe.throw(_("Time logs are required for {0} {1}")
|
||||||
.format(frappe.bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
||||||
|
|
||||||
if self.for_quantity and self.total_completed_qty != self.for_quantity:
|
if self.for_quantity and self.total_completed_qty != self.for_quantity:
|
||||||
total_completed_qty = frappe.bold(_("Total Completed Qty"))
|
total_completed_qty = bold(_("Total Completed Qty"))
|
||||||
qty_to_manufacture = frappe.bold(_("Qty to Manufacture"))
|
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||||
|
|
||||||
frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})"
|
frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})")
|
||||||
.format(total_completed_qty, frappe.bold(self.total_completed_qty), qty_to_manufacture,frappe.bold(self.for_quantity))))
|
.format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity)))
|
||||||
|
|
||||||
def update_work_order(self):
|
def update_work_order(self):
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
@ -213,10 +215,7 @@ class JobCard(Document):
|
|||||||
from_time_list, to_time_list = [], []
|
from_time_list, to_time_list = [], []
|
||||||
|
|
||||||
field = "operation_id"
|
field = "operation_id"
|
||||||
data = frappe.get_all('Job Card',
|
data = self.get_current_operation_data()
|
||||||
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
|
||||||
filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)})
|
|
||||||
|
|
||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
for_quantity = data[0].completed_qty
|
for_quantity = data[0].completed_qty
|
||||||
time_in_mins = data[0].time_in_mins
|
time_in_mins = data[0].time_in_mins
|
||||||
@ -246,6 +245,11 @@ class JobCard(Document):
|
|||||||
wo.set_actual_dates()
|
wo.set_actual_dates()
|
||||||
wo.save()
|
wo.save()
|
||||||
|
|
||||||
|
def get_current_operation_data(self):
|
||||||
|
return frappe.get_all('Job Card',
|
||||||
|
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||||
|
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
if not self.items:
|
if not self.items:
|
||||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||||
@ -310,9 +314,32 @@ class JobCard(Document):
|
|||||||
def validate_operation_id(self):
|
def validate_operation_id(self):
|
||||||
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
|
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
|
||||||
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
|
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
|
||||||
work_order = frappe.bold(get_link_to_form("Work Order", self.work_order))
|
work_order = bold(get_link_to_form("Work Order", self.work_order))
|
||||||
frappe.throw(_("Operation {0} does not belong to the work order {1}")
|
frappe.throw(_("Operation {0} does not belong to the work order {1}")
|
||||||
.format(frappe.bold(self.operation), work_order), OperationMismatchError)
|
.format(bold(self.operation), work_order), OperationMismatchError)
|
||||||
|
|
||||||
|
def validate_sequence_id(self):
|
||||||
|
if not (self.work_order and self.sequence_id): return
|
||||||
|
|
||||||
|
current_operation_qty = 0.0
|
||||||
|
data = self.get_current_operation_data()
|
||||||
|
if data and len(data) > 0:
|
||||||
|
current_operation_qty = flt(data[0].completed_qty)
|
||||||
|
|
||||||
|
current_operation_qty += flt(self.total_completed_qty)
|
||||||
|
|
||||||
|
data = frappe.get_all("Work Order Operation",
|
||||||
|
fields = ["operation", "status", "completed_qty"],
|
||||||
|
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)},
|
||||||
|
order_by = "sequence_id, idx")
|
||||||
|
|
||||||
|
message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name),
|
||||||
|
bold(get_link_to_form("Work Order", self.work_order)))
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
if row.status != "Completed" and row.completed_qty < current_operation_qty:
|
||||||
|
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
||||||
|
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_operation_details(work_order, operation):
|
def get_operation_details(work_order, operation):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2014-11-27 14:12:07.542534",
|
"creation": "2014-11-27 14:12:07.542534",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
@ -36,7 +37,7 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:!doc.disable_capacity_planning",
|
"depends_on": "eval:!doc.disable_capacity_planning",
|
||||||
"description": "Plan time logs outside Workstation Working Hours.",
|
"description": "Plan time logs outside Workstation working hours",
|
||||||
"fieldname": "allow_overtime",
|
"fieldname": "allow_overtime",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Overtime"
|
"label": "Allow Overtime"
|
||||||
@ -56,17 +57,17 @@
|
|||||||
{
|
{
|
||||||
"default": "30",
|
"default": "30",
|
||||||
"depends_on": "eval:!doc.disable_capacity_planning",
|
"depends_on": "eval:!doc.disable_capacity_planning",
|
||||||
"description": "Try planning operations for X days in advance.",
|
"description": "Plan operations X days in advance",
|
||||||
"fieldname": "capacity_planning_for_days",
|
"fieldname": "capacity_planning_for_days",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Capacity Planning For (Days)"
|
"label": "Capacity Planning For (Days)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.disable_capacity_planning",
|
"depends_on": "eval:!doc.disable_capacity_planning",
|
||||||
"description": "Default 10 mins",
|
"description": "Default: 10 mins",
|
||||||
"fieldname": "mins_between_operations",
|
"fieldname": "mins_between_operations",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Time Between Operations (in mins)"
|
"label": "Time Between Operations (Mins)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
@ -92,14 +93,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Allow multiple Material Consumption against a Work Order",
|
"description": "Allow multiple material consumptions against a Work Order",
|
||||||
"fieldname": "material_consumption",
|
"fieldname": "material_consumption",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Multiple Material Consumption"
|
"label": "Allow Multiple Material Consumption"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.",
|
"description": "Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials",
|
||||||
"fieldname": "update_bom_costs_automatically",
|
"fieldname": "update_bom_costs_automatically",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Update BOM Cost Automatically"
|
"label": "Update BOM Cost Automatically"
|
||||||
@ -135,7 +136,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "over_production_for_sales_and_work_order_section",
|
"fieldname": "over_production_for_sales_and_work_order_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Over Production for Sales and Work Order"
|
"label": "Overproduction for Sales and Work Order"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "raw_materials_consumption_section",
|
"fieldname": "raw_materials_consumption_section",
|
||||||
@ -157,8 +158,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"modified": "2019-11-26 13:10:45.569341",
|
"links": [],
|
||||||
|
"modified": "2020-10-13 10:55:43.996581",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
@ -175,4 +178,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -9,3 +9,23 @@ test_records = frappe.get_test_records('Operation')
|
|||||||
|
|
||||||
class TestOperation(unittest.TestCase):
|
class TestOperation(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def make_operation(*args, **kwargs):
|
||||||
|
args = args if args else kwargs
|
||||||
|
if isinstance(args, tuple):
|
||||||
|
args = args[0]
|
||||||
|
|
||||||
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Operation",
|
||||||
|
"name": args.operation,
|
||||||
|
"workstation": args.workstation
|
||||||
|
})
|
||||||
|
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
return doc
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
return frappe.get_doc("Operation", args.operation)
|
@ -237,7 +237,9 @@ def make_bom(**args):
|
|||||||
'item': args.item,
|
'item': args.item,
|
||||||
'currency': args.currency or 'USD',
|
'currency': args.currency or 'USD',
|
||||||
'quantity': args.quantity or 1,
|
'quantity': args.quantity or 1,
|
||||||
'company': args.company or '_Test Company'
|
'company': args.company or '_Test Company',
|
||||||
|
'routing': args.routing,
|
||||||
|
'with_operations': args.with_operations or 0
|
||||||
})
|
})
|
||||||
|
|
||||||
for item in args.raw_materials:
|
for item in args.raw_materials:
|
||||||
|
@ -2,6 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Routing', {
|
frappe.ui.form.on('Routing', {
|
||||||
|
setup: function(frm) {
|
||||||
|
frappe.meta.get_docfield("BOM Operation", "sequence_id",
|
||||||
|
frm.doc.name).in_list_view = true;
|
||||||
|
|
||||||
|
frm.fields_dict.operations.grid.refresh();
|
||||||
|
},
|
||||||
|
|
||||||
calculate_operating_cost: function(frm, child) {
|
calculate_operating_cost: function(frm, child) {
|
||||||
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
|
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
|
||||||
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
|
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
|
||||||
|
@ -3,7 +3,22 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class Routing(Document):
|
class Routing(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
self.set_routing_id()
|
||||||
|
|
||||||
|
def set_routing_id(self):
|
||||||
|
sequence_id = 0
|
||||||
|
for row in self.operations:
|
||||||
|
if not row.sequence_id:
|
||||||
|
row.sequence_id = sequence_id + 1
|
||||||
|
elif sequence_id and row.sequence_id and cint(sequence_id) > cint(row.sequence_id):
|
||||||
|
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
|
||||||
|
.format(row.idx, row.sequence_id, sequence_id))
|
||||||
|
|
||||||
|
sequence_id = row.sequence_id
|
@ -4,6 +4,88 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import frappe
|
||||||
|
from frappe.test_runner import make_test_records
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||||
|
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||||
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
|
||||||
class TestRouting(unittest.TestCase):
|
class TestRouting(unittest.TestCase):
|
||||||
pass
|
def test_sequence_id(self):
|
||||||
|
item_code = "Test Routing Item - A"
|
||||||
|
operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30},
|
||||||
|
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}]
|
||||||
|
|
||||||
|
make_test_records("UOM")
|
||||||
|
|
||||||
|
setup_operations(operations)
|
||||||
|
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||||
|
bom_doc = setup_bom(item_code=item_code, routing=routing_doc.name)
|
||||||
|
wo_doc = make_wo_order_test_record(production_item = item_code, bom_no=bom_doc.name)
|
||||||
|
|
||||||
|
for row in routing_doc.operations:
|
||||||
|
self.assertEqual(row.sequence_id, row.idx)
|
||||||
|
|
||||||
|
for data in frappe.get_all("Job Card",
|
||||||
|
filters={"work_order": wo_doc.name}, order_by="sequence_id desc"):
|
||||||
|
job_card_doc = frappe.get_doc("Job Card", data.name)
|
||||||
|
job_card_doc.time_logs[0].completed_qty = 10
|
||||||
|
if job_card_doc.sequence_id != 1:
|
||||||
|
self.assertRaises(OperationSequenceError, job_card_doc.save)
|
||||||
|
else:
|
||||||
|
job_card_doc.save()
|
||||||
|
self.assertEqual(job_card_doc.total_completed_qty, 10)
|
||||||
|
|
||||||
|
wo_doc.cancel()
|
||||||
|
wo_doc.delete()
|
||||||
|
|
||||||
|
def setup_operations(rows):
|
||||||
|
for row in rows:
|
||||||
|
make_workstation(row)
|
||||||
|
make_operation(row)
|
||||||
|
|
||||||
|
def create_routing(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
doc = frappe.new_doc("Routing")
|
||||||
|
doc.update(args)
|
||||||
|
|
||||||
|
if not args.do_not_save:
|
||||||
|
try:
|
||||||
|
for operation in args.operations:
|
||||||
|
doc.append("operations", operation)
|
||||||
|
|
||||||
|
doc.insert()
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
doc = frappe.get_doc("Routing", args.routing_name)
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def setup_bom(**args):
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
|
||||||
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
if not frappe.db.exists('Item', args.item_code):
|
||||||
|
make_item(args.item_code, {
|
||||||
|
'is_stock_item': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if not args.raw_materials:
|
||||||
|
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||||
|
make_item("Test Extra Item N-1", {
|
||||||
|
'is_stock_item': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
args.raw_materials = ['Test Extra Item N-1']
|
||||||
|
|
||||||
|
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
|
||||||
|
if not name:
|
||||||
|
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
|
||||||
|
routing = args.routing, with_operations=1)
|
||||||
|
else:
|
||||||
|
bom_doc = frappe.get_doc("BOM", name)
|
||||||
|
|
||||||
|
return bom_doc
|
@ -193,6 +193,42 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
self.assertEqual(cint(bin1_on_end_production.projected_qty),
|
self.assertEqual(cint(bin1_on_end_production.projected_qty),
|
||||||
cint(bin1_on_end_production.projected_qty))
|
cint(bin1_on_end_production.projected_qty))
|
||||||
|
|
||||||
|
def test_backflush_qty_for_overpduction_manufacture(self):
|
||||||
|
cancel_stock_entry = []
|
||||||
|
allow_overproduction("overproduction_percentage_for_work_order", 30)
|
||||||
|
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100)
|
||||||
|
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||||
|
target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0)
|
||||||
|
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
|
||||||
|
target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0)
|
||||||
|
|
||||||
|
cancel_stock_entry.extend([ste1.name, ste2.name])
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 50))
|
||||||
|
s1.submit()
|
||||||
|
cancel_stock_entry.append(s1.name)
|
||||||
|
|
||||||
|
self.assertEqual(s1.items[0].qty, 50)
|
||||||
|
self.assertEqual(s1.items[1].qty, 100)
|
||||||
|
cancel_stock_entry.reverse()
|
||||||
|
for ste in cancel_stock_entry:
|
||||||
|
doc = frappe.get_doc("Stock Entry", ste)
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
|
allow_overproduction("overproduction_percentage_for_work_order", 0)
|
||||||
|
|
||||||
def test_reserved_qty_for_stopped_production(self):
|
def test_reserved_qty_for_stopped_production(self):
|
||||||
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||||
target= self.warehouse, qty=100, basic_rate=100)
|
target= self.warehouse, qty=100, basic_rate=100)
|
||||||
|
@ -378,7 +378,7 @@ class WorkOrder(Document):
|
|||||||
select
|
select
|
||||||
operation, description, workstation, idx,
|
operation, description, workstation, idx,
|
||||||
base_hour_rate as hour_rate, time_in_mins,
|
base_hour_rate as hour_rate, time_in_mins,
|
||||||
"Pending" as status, parent as bom, batch_size
|
"Pending" as status, parent as bom, batch_size, sequence_id
|
||||||
from
|
from
|
||||||
`tabBOM Operation`
|
`tabBOM Operation`
|
||||||
where
|
where
|
||||||
@ -865,6 +865,7 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto
|
|||||||
'bom_no': work_order.bom_no,
|
'bom_no': work_order.bom_no,
|
||||||
'project': work_order.project,
|
'project': work_order.project,
|
||||||
'company': work_order.company,
|
'company': work_order.company,
|
||||||
|
'sequence_id': row.get("sequence_id"),
|
||||||
'wip_warehouse': work_order.wip_warehouse
|
'wip_warehouse': work_order.wip_warehouse
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"details",
|
"details",
|
||||||
"operation",
|
"operation",
|
||||||
"bom",
|
"bom",
|
||||||
|
"sequence_id",
|
||||||
"description",
|
"description",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"completed_qty",
|
"completed_qty",
|
||||||
@ -187,11 +188,19 @@
|
|||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Batch Size",
|
"label": "Batch Size",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sequence_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Sequence ID",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2019-12-03 19:24:29.594189",
|
"modified": "2020-10-14 12:58:49.241252",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Operation",
|
"name": "Work Order Operation",
|
||||||
|
@ -21,17 +21,22 @@ class TestWorkstation(unittest.TestCase):
|
|||||||
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
|
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
|
||||||
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
|
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
|
||||||
|
|
||||||
def make_workstation(**args):
|
def make_workstation(*args, **kwargs):
|
||||||
|
args = args if args else kwargs
|
||||||
|
if isinstance(args, tuple):
|
||||||
|
args = args[0]
|
||||||
|
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
workstation_name = args.workstation_name or args.workstation
|
||||||
try:
|
try:
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
"doctype": "Workstation",
|
"doctype": "Workstation",
|
||||||
"workstation_name": args.workstation_name
|
"workstation_name": workstation_name
|
||||||
})
|
})
|
||||||
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
return frappe.get_doc("Workstation", args.workstation_name)
|
return frappe.get_doc("Workstation", workstation_name)
|
@ -729,4 +729,6 @@ erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
|
|||||||
erpnext.patches.v13_0.rename_issue_doctype_fields
|
erpnext.patches.v13_0.rename_issue_doctype_fields
|
||||||
erpnext.patches.v13_0.change_default_pos_print_format
|
erpnext.patches.v13_0.change_default_pos_print_format
|
||||||
erpnext.patches.v13_0.set_youtube_video_id
|
erpnext.patches.v13_0.set_youtube_video_id
|
||||||
erpnext.patches.v13_0.print_uom_after_quantity_patch
|
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
|
||||||
|
@ -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'])
|
@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Set the payment gateway account as Email for all the existing payment channel."""
|
||||||
|
doc_meta = frappe.get_meta("Payment Gateway Account")
|
||||||
|
if doc_meta.get_field("payment_channel"):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.reload_doc("Accounts", "doctype", "Payment Gateway Account")
|
||||||
|
set_payment_channel_as_email()
|
||||||
|
|
||||||
|
def set_payment_channel_as_email():
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabPayment Gateway Account`
|
||||||
|
SET `payment_channel` = "Email"
|
||||||
|
""")
|
@ -162,7 +162,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
|
|
||||||
// sales invoice
|
// sales invoice
|
||||||
if(flt(doc.per_billed, 6) < 100) {
|
if(flt(doc.per_billed, 6) < 100) {
|
||||||
this.frm.add_custom_button(__('Invoice'), () => me.make_sales_invoice(), __('Create'));
|
this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// material request
|
// material request
|
||||||
@ -554,19 +554,32 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
},
|
},
|
||||||
|
|
||||||
make_purchase_order: function(){
|
make_purchase_order: function(){
|
||||||
|
let pending_items = this.frm.doc.items.some((item) =>{
|
||||||
|
let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty);
|
||||||
|
return pending_qty > 0;
|
||||||
|
})
|
||||||
|
if(!pending_items){
|
||||||
|
frappe.throw({message: __("Purchase Order already created for all Sales Order items"), title: __("Note")});
|
||||||
|
}
|
||||||
|
|
||||||
var me = this;
|
var me = this;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("For Supplier"),
|
title: __("Select Items"),
|
||||||
fields: [
|
fields: [
|
||||||
{"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier",
|
{
|
||||||
"description": __("Leave the field empty to make purchase orders for all suppliers"),
|
"fieldtype": "Check",
|
||||||
"get_query": function () {
|
"label": __("Against Default Supplier"),
|
||||||
return {
|
"fieldname": "against_default_supplier",
|
||||||
query:"erpnext.selling.doctype.sales_order.sales_order.get_supplier",
|
"default": 0
|
||||||
filters: {'parent': me.frm.doc.name}
|
},
|
||||||
}
|
{
|
||||||
}},
|
"fieldtype": "Section Break",
|
||||||
{fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
"label": "",
|
||||||
|
"fieldname": "sec_break_dialog",
|
||||||
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldtype:'Data',
|
fieldtype:'Data',
|
||||||
@ -584,8 +597,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype:'Float',
|
fieldtype:'Float',
|
||||||
fieldname:'qty',
|
fieldname:'pending_qty',
|
||||||
label: __('Quantity'),
|
label: __('Pending Qty'),
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
in_list_view:1
|
in_list_view:1
|
||||||
},
|
},
|
||||||
@ -594,60 +607,86 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
read_only:1,
|
read_only:1,
|
||||||
fieldname:'uom',
|
fieldname:'uom',
|
||||||
label: __('UOM'),
|
label: __('UOM'),
|
||||||
|
in_list_view:1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:'Data',
|
||||||
|
fieldname:'supplier',
|
||||||
|
label: __('Supplier'),
|
||||||
|
read_only:1,
|
||||||
in_list_view:1
|
in_list_view:1
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
data: cur_frm.doc.items,
|
data: me.frm.doc.items.map((item) =>{
|
||||||
get_data: function() {
|
item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor);
|
||||||
return cur_frm.doc.items
|
return item;
|
||||||
}
|
}).filter((item) => {return item.pending_qty > 0;})
|
||||||
},
|
|
||||||
|
|
||||||
{"fieldtype": "Button", "label": __('Create Purchase Order'), "fieldname": "make_purchase_order", "cssClass": "btn-primary"},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.fields_dict.make_purchase_order.$input.click(function() {
|
|
||||||
var args = dialog.get_values();
|
|
||||||
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children()
|
|
||||||
if(selected_items.length == 0) {
|
|
||||||
frappe.throw({message: 'Please select Item form Table', title: __('Message'), indicator:'blue'})
|
|
||||||
}
|
|
||||||
let selected_items_list = []
|
|
||||||
for(let i in selected_items){
|
|
||||||
selected_items_list.push(selected_items[i].item_code)
|
|
||||||
}
|
|
||||||
dialog.hide();
|
|
||||||
return frappe.call({
|
|
||||||
type: "GET",
|
|
||||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_purchase_order",
|
|
||||||
args: {
|
|
||||||
"source_name": me.frm.doc.name,
|
|
||||||
"for_supplier": args.supplier,
|
|
||||||
"selected_items": selected_items_list
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function(r) {
|
|
||||||
if(!r.exc) {
|
|
||||||
// var args = dialog.get_values();
|
|
||||||
if (args.supplier){
|
|
||||||
var doc = frappe.model.sync(r.message);
|
|
||||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
frappe.route_options = {
|
|
||||||
"sales_order": me.frm.doc.name
|
|
||||||
}
|
|
||||||
frappe.set_route("List", "Purchase Order");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
],
|
||||||
|
primary_action_label: 'Create Purchase Order',
|
||||||
|
primary_action (args) {
|
||||||
|
if (!args) return;
|
||||||
|
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children();
|
||||||
|
if(selected_items.length == 0) {
|
||||||
|
frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'})
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order"
|
||||||
|
return frappe.call({
|
||||||
|
type: "GET",
|
||||||
|
method: "erpnext.selling.doctype.sales_order.sales_order." + method,
|
||||||
|
args: {
|
||||||
|
"source_name": me.frm.doc.name,
|
||||||
|
"selected_items": selected_items
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
if (!args.against_default_supplier) {
|
||||||
|
frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frappe.route_options = {
|
||||||
|
"sales_order": me.frm.doc.name
|
||||||
|
}
|
||||||
|
frappe.set_route("List", "Purchase Order");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
dialog.get_field("items_for_po").grid.only_sortable()
|
|
||||||
dialog.get_field("items_for_po").refresh()
|
dialog.fields_dict["against_default_supplier"].df.onchange = () => {
|
||||||
|
console.log("yo");
|
||||||
|
var against_default_supplier = dialog.get_value("against_default_supplier");
|
||||||
|
var items_for_po = dialog.get_value("items_for_po");
|
||||||
|
|
||||||
|
if (against_default_supplier) {
|
||||||
|
let items_with_supplier = items_for_po.filter((item) => item.supplier)
|
||||||
|
|
||||||
|
dialog.fields_dict["items_for_po"].df.data = items_with_supplier;
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
} else {
|
||||||
|
let pending_items = me.frm.doc.items.map((item) =>{
|
||||||
|
item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor);
|
||||||
|
return item;
|
||||||
|
}).filter((item) => {return item.pending_qty > 0;});
|
||||||
|
|
||||||
|
dialog.fields_dict["items_for_po"].df.data = pending_items;
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.get_field("items_for_po").grid.only_sortable();
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
dialog.wrapper.find('.grid-heading-row .grid-row-check').click();
|
||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
hold_sales_order: function(){
|
hold_sales_order: function(){
|
||||||
var me = this;
|
var me = this;
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
|
@ -443,25 +443,19 @@ class SalesOrder(SellingController):
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.ensure_delivery_based_on_produced_serial_no:
|
if item.ensure_delivery_based_on_produced_serial_no:
|
||||||
if item.item_code in normal_items:
|
if item.item_code in normal_items:
|
||||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code))
|
||||||
Item {0} is added with and without Ensure Delivery by \
|
|
||||||
Serial No.").format(item.item_code))
|
|
||||||
if item.item_code not in reserved_items:
|
if item.item_code not in reserved_items:
|
||||||
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
||||||
frappe.throw(_("Item {0} has no Serial No. Only serilialized items \
|
frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code))
|
||||||
can have delivery based on Serial No").format(item.item_code))
|
|
||||||
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
||||||
frappe.throw(_("No active BOM found for item {0}. Delivery by \
|
frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code))
|
||||||
Serial No cannot be ensured").format(item.item_code))
|
|
||||||
reserved_items.append(item.item_code)
|
reserved_items.append(item.item_code)
|
||||||
else:
|
else:
|
||||||
normal_items.append(item.item_code)
|
normal_items.append(item.item_code)
|
||||||
|
|
||||||
if not item.ensure_delivery_based_on_produced_serial_no and \
|
if not item.ensure_delivery_based_on_produced_serial_no and \
|
||||||
item.item_code in reserved_items:
|
item.item_code in reserved_items:
|
||||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code))
|
||||||
Item {0} is added with and without Ensure Delivery by \
|
|
||||||
Serial No.").format(item.item_code))
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||||
@ -785,7 +779,7 @@ def get_events(start, end, filters=None):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_purchase_order(source_name, for_supplier=None, selected_items=[], target_doc=None):
|
def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None):
|
||||||
if isinstance(selected_items, string_types):
|
if isinstance(selected_items, string_types):
|
||||||
selected_items = json.loads(selected_items)
|
selected_items = json.loads(selected_items)
|
||||||
|
|
||||||
@ -822,24 +816,21 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
|
|
||||||
def update_item(source, target, source_parent):
|
def update_item(source, target, source_parent):
|
||||||
target.schedule_date = source.delivery_date
|
target.schedule_date = source.delivery_date
|
||||||
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor))
|
||||||
target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor)
|
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||||
target.project = source_parent.project
|
target.project = source_parent.project
|
||||||
|
|
||||||
suppliers =[]
|
suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
|
||||||
if for_supplier:
|
suppliers = list(set(suppliers))
|
||||||
suppliers.append(for_supplier)
|
|
||||||
else:
|
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||||
sales_order = frappe.get_doc("Sales Order", source_name)
|
items_to_map = list(set(items_to_map))
|
||||||
for item in sales_order.items:
|
|
||||||
if item.supplier and item.supplier not in suppliers:
|
|
||||||
suppliers.append(item.supplier)
|
|
||||||
|
|
||||||
if not suppliers:
|
if not suppliers:
|
||||||
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
|
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
|
||||||
|
|
||||||
for supplier in suppliers:
|
for supplier in suppliers:
|
||||||
po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||||
if len(po) == 0:
|
if len(po) == 0:
|
||||||
doc = get_mapped_doc("Sales Order", source_name, {
|
doc = get_mapped_doc("Sales Order", source_name, {
|
||||||
"Sales Order": {
|
"Sales Order": {
|
||||||
@ -850,7 +841,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
"contact_mobile",
|
"contact_mobile",
|
||||||
"contact_email",
|
"contact_email",
|
||||||
"contact_person",
|
"contact_person",
|
||||||
"taxes_and_charges"
|
"taxes_and_charges",
|
||||||
|
"shipping_address"
|
||||||
],
|
],
|
||||||
"validation": {
|
"validation": {
|
||||||
"docstatus": ["=", 1]
|
"docstatus": ["=", 1]
|
||||||
@ -872,52 +864,82 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
"item_tax_template"
|
"item_tax_template"
|
||||||
],
|
],
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items
|
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
|
||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
if not for_supplier:
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
else:
|
else:
|
||||||
suppliers =[]
|
suppliers =[]
|
||||||
if suppliers:
|
if suppliers:
|
||||||
if not for_supplier:
|
frappe.db.commit()
|
||||||
frappe.db.commit()
|
|
||||||
return doc
|
return doc
|
||||||
else:
|
else:
|
||||||
frappe.msgprint(_("PO already created for all sales order items"))
|
frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
def make_purchase_order(source_name, selected_items=[], target_doc=None):
|
||||||
def get_supplier(doctype, txt, searchfield, start, page_len, filters):
|
if isinstance(selected_items, string_types):
|
||||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
selected_items = json.loads(selected_items)
|
||||||
if supp_master_name == "Supplier Name":
|
|
||||||
fields = ["name", "supplier_group"]
|
|
||||||
else:
|
|
||||||
fields = ["name", "supplier_name", "supplier_group"]
|
|
||||||
fields = ", ".join(fields)
|
|
||||||
|
|
||||||
return frappe.db.sql("""select {field} from `tabSupplier`
|
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||||
where docstatus < 2
|
items_to_map = list(set(items_to_map))
|
||||||
and ({key} like %(txt)s
|
|
||||||
or supplier_name like %(txt)s)
|
def set_missing_values(source, target):
|
||||||
and name in (select supplier from `tabSales Order Item` where parent = %(parent)s)
|
target.supplier = ""
|
||||||
and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi
|
target.apply_discount_on = ""
|
||||||
on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s)
|
target.additional_discount_percentage = 0.0
|
||||||
order by
|
target.discount_amount = 0.0
|
||||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
target.inter_company_order_reference = ""
|
||||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
target.customer = ""
|
||||||
name, supplier_name
|
target.customer_name = ""
|
||||||
limit %(start)s, %(page_len)s """.format(**{
|
target.run_method("set_missing_values")
|
||||||
'field': fields,
|
target.run_method("calculate_taxes_and_totals")
|
||||||
'key': frappe.db.escape(searchfield)
|
|
||||||
}), {
|
def update_item(source, target, source_parent):
|
||||||
'txt': "%%%s%%" % txt,
|
target.schedule_date = source.delivery_date
|
||||||
'_txt': txt.replace("%", ""),
|
target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor))
|
||||||
'start': start,
|
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||||
'page_len': page_len,
|
target.project = source_parent.project
|
||||||
'parent': filters.get('parent')
|
|
||||||
})
|
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||||
|
doc = get_mapped_doc("Sales Order", source_name, {
|
||||||
|
"Sales Order": {
|
||||||
|
"doctype": "Purchase Order",
|
||||||
|
"field_no_map": [
|
||||||
|
"address_display",
|
||||||
|
"contact_display",
|
||||||
|
"contact_mobile",
|
||||||
|
"contact_email",
|
||||||
|
"contact_person",
|
||||||
|
"taxes_and_charges",
|
||||||
|
"shipping_address"
|
||||||
|
],
|
||||||
|
"validation": {
|
||||||
|
"docstatus": ["=", 1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Sales Order Item": {
|
||||||
|
"doctype": "Purchase Order Item",
|
||||||
|
"field_map": [
|
||||||
|
["name", "sales_order_item"],
|
||||||
|
["parent", "sales_order"],
|
||||||
|
["stock_uom", "stock_uom"],
|
||||||
|
["uom", "uom"],
|
||||||
|
["conversion_factor", "conversion_factor"],
|
||||||
|
["delivery_date", "schedule_date"]
|
||||||
|
],
|
||||||
|
"field_no_map": [
|
||||||
|
"rate",
|
||||||
|
"price_list_rate",
|
||||||
|
"item_tax_template",
|
||||||
|
"supplier"
|
||||||
|
],
|
||||||
|
"postprocess": update_item,
|
||||||
|
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map
|
||||||
|
}
|
||||||
|
}, target_doc, set_missing_values)
|
||||||
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_work_orders(items, sales_order, company, project=None):
|
def make_work_orders(items, sales_order, company, project=None):
|
||||||
|
@ -688,12 +688,12 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
|
||||||
def test_drop_shipping(self):
|
def test_drop_shipping(self):
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
|
||||||
|
update_status as so_update_status
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
||||||
|
|
||||||
make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100)
|
# make items
|
||||||
po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||||
|
|
||||||
dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
|
dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
|
||||||
|
|
||||||
so_items = [
|
so_items = [
|
||||||
@ -715,80 +715,61 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1:
|
if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1:
|
||||||
make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=10, rate=100)
|
make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100)
|
||||||
|
|
||||||
#setuo existing qty from bin
|
#create so, po and dn
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
|
||||||
|
|
||||||
existing_ordered_qty = bin[0].ordered_qty if bin else 0.0
|
|
||||||
existing_reserved_qty = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": dn_item.item_code,
|
|
||||||
"warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"])
|
|
||||||
|
|
||||||
existing_reserved_qty_for_dn_item = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
#create so, po and partial dn
|
|
||||||
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
||||||
so.submit()
|
so.submit()
|
||||||
|
|
||||||
po = make_purchase_order(so.name, '_Test Supplier', selected_items=[so_items[0]['item_code']])
|
po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
|
||||||
po.submit()
|
po.submit()
|
||||||
|
|
||||||
dn = create_dn_against_so(so.name, delivered_qty=1)
|
dn = create_dn_against_so(so.name, delivered_qty=2)
|
||||||
|
|
||||||
self.assertEqual(so.customer, po.customer)
|
self.assertEqual(so.customer, po.customer)
|
||||||
self.assertEqual(po.items[0].sales_order, so.name)
|
self.assertEqual(po.items[0].sales_order, so.name)
|
||||||
self.assertEqual(po.items[0].item_code, po_item.item_code)
|
self.assertEqual(po.items[0].item_code, po_item.item_code)
|
||||||
self.assertEqual(dn.items[0].item_code, dn_item.item_code)
|
self.assertEqual(dn.items[0].item_code, dn_item.item_code)
|
||||||
|
|
||||||
#test ordered_qty and reserved_qty
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
|
||||||
|
|
||||||
ordered_qty = bin[0].ordered_qty if bin else 0.0
|
|
||||||
reserved_qty = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty)
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty)
|
|
||||||
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item + 1)
|
|
||||||
|
|
||||||
#test po_item length
|
#test po_item length
|
||||||
self.assertEqual(len(po.items), 1)
|
self.assertEqual(len(po.items), 1)
|
||||||
|
|
||||||
#test per_delivered status
|
# test ordered_qty and reserved_qty for drop ship item
|
||||||
|
bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
||||||
|
fields=["ordered_qty", "reserved_qty"])
|
||||||
|
|
||||||
|
ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0
|
||||||
|
reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0
|
||||||
|
|
||||||
|
# drop ship PO should not impact bin, test the same
|
||||||
|
self.assertEqual(abs(flt(ordered_qty)), 0)
|
||||||
|
self.assertEqual(abs(flt(reserved_qty)), 0)
|
||||||
|
|
||||||
|
# test per_delivered status
|
||||||
update_status("Delivered", po.name)
|
update_status("Delivered", po.name)
|
||||||
self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 75.00)
|
self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 100.00)
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
#test reserved qty after complete delivery
|
# test after closing so
|
||||||
dn = create_dn_against_so(so.name, delivered_qty=1)
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item)
|
|
||||||
|
|
||||||
#test after closing so
|
|
||||||
so.db_set('status', "Closed")
|
so.db_set('status', "Closed")
|
||||||
so.update_reserved_qty()
|
so.update_reserved_qty()
|
||||||
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
# test ordered_qty and reserved_qty for drop ship item after closing so
|
||||||
|
bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
fields=["ordered_qty", "reserved_qty"])
|
||||||
|
|
||||||
ordered_qty = bin[0].ordered_qty if bin else 0.0
|
ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0
|
||||||
reserved_qty = bin[0].reserved_qty if bin else 0.0
|
reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0
|
||||||
|
|
||||||
self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty)
|
self.assertEqual(abs(flt(ordered_qty)), 0)
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty)
|
self.assertEqual(abs(flt(reserved_qty)), 0)
|
||||||
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
# teardown
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
so_update_status("Draft", so.name)
|
||||||
|
dn.load_from_db()
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item)
|
dn.cancel()
|
||||||
|
po.cancel()
|
||||||
|
so.load_from_db()
|
||||||
|
so.cancel()
|
||||||
|
|
||||||
def test_reserved_qty_for_closing_so(self):
|
def test_reserved_qty_for_closing_so(self):
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "15",
|
"default": "15",
|
||||||
"description": "Auto close Opportunity after 15 days",
|
"description": "Auto close Opportunity after the no. of days mentioned above",
|
||||||
"fieldname": "close_opportunity_after_days",
|
"fieldname": "close_opportunity_after_days",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Close Opportunity After Days"
|
"label": "Close Opportunity After Days"
|
||||||
@ -80,18 +80,18 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "so_required",
|
"fieldname": "so_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Sales Order Required for Sales Invoice & Delivery Note Creation",
|
"label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "dn_required",
|
"fieldname": "dn_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Delivery Note Required for Sales Invoice Creation",
|
"label": "Is Delivery Note Required for Sales Invoice Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Each Transaction",
|
"default": "Each Transaction",
|
||||||
"description": "How often should project and company be updated based on Sales Transactions.",
|
"description": "How often should Project and Company be updated based on Sales Transactions?",
|
||||||
"fieldname": "sales_update_frequency",
|
"fieldname": "sales_update_frequency",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Sales Update Frequency",
|
"label": "Sales Update Frequency",
|
||||||
@ -108,38 +108,39 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "editable_price_list_rate",
|
"fieldname": "editable_price_list_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow user to edit Price List Rate in transactions"
|
"label": "Allow User to Edit Price List Rate in Transactions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_multiple_items",
|
"fieldname": "allow_multiple_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Item to be added multiple times in a transaction"
|
"label": "Allow Item to Be Added Multiple Times in a Transaction"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_against_multiple_purchase_orders",
|
"fieldname": "allow_against_multiple_purchase_orders",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow multiple Sales Orders against a Customer's Purchase Order"
|
"label": "Allow Multiple Sales Orders Against a Customer's Purchase Order"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "validate_selling_price",
|
"fieldname": "validate_selling_price",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Validate Selling Price for Item against Purchase Rate or Valuation Rate"
|
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "hide_tax_id",
|
"fieldname": "hide_tax_id",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Hide Customer's Tax Id from Sales Transactions"
|
"label": "Hide Customer's Tax ID from Sales Transactions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-01 13:58:35.637858",
|
"modified": "2020-10-13 12:12:56.784014",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
@ -9,8 +9,8 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init_component() {
|
init_component() {
|
||||||
this.prepare_dom();
|
this.prepare_dom();
|
||||||
this.initialize_numpad();
|
this.initialize_numpad();
|
||||||
this.bind_events();
|
this.bind_events();
|
||||||
this.attach_shortcuts();
|
this.attach_shortcuts();
|
||||||
|
|
||||||
@ -18,32 +18,32 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
|
|
||||||
prepare_dom() {
|
prepare_dom() {
|
||||||
this.wrapper.append(
|
this.wrapper.append(
|
||||||
`<section class="col-span-6 flex shadow rounded payment-section bg-white mx-h-70 h-100 d-none">
|
`<section class="col-span-6 flex shadow rounded payment-section bg-white mx-h-70 h-100 d-none">
|
||||||
<div class="flex flex-col p-16 pt-8 pb-8 w-full">
|
<div class="flex flex-col p-16 pt-8 pb-8 w-full">
|
||||||
<div class="text-grey mb-6 payment-section no-select pointer">
|
<div class="text-grey mb-6 payment-section no-select pointer">
|
||||||
PAYMENT METHOD<span class="octicon octicon-chevron-down collapse-indicator"></span>
|
PAYMENT METHOD<span class="octicon octicon-chevron-down collapse-indicator"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="payment-modes flex flex-wrap"></div>
|
<div class="payment-modes flex flex-wrap"></div>
|
||||||
<div class="invoice-details-section"></div>
|
<div class="invoice-details-section"></div>
|
||||||
<div class="flex mt-auto justify-center w-full">
|
<div class="flex mt-auto justify-center w-full">
|
||||||
<div class="flex flex-col justify-center flex-1 ml-4">
|
<div class="flex flex-col justify-center flex-1 ml-4">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="totals-remarks items-end justify-end flex flex-1">
|
<div class="totals-remarks items-end justify-end flex flex-1">
|
||||||
<div class="remarks text-md-0 text-grey mr-auto"></div>
|
<div class="remarks text-md-0 text-grey mr-auto"></div>
|
||||||
<div class="totals flex justify-end pt-4"></div>
|
<div class="totals flex justify-end pt-4"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="number-pad w-40 mb-4 ml-8 d-none"></div>
|
<div class="number-pad w-40 mb-4 ml-8 d-none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center mt-4 submit-order h-16 w-full rounded bg-primary text-md text-white no-select pointer text-bold">
|
<div class="flex items-center justify-center mt-4 submit-order h-16 w-full rounded bg-primary text-md text-white no-select pointer text-bold">
|
||||||
Complete Order
|
Complete Order
|
||||||
</div>
|
</div>
|
||||||
<div class="order-time flex items-center justify-end mt-2 pt-2 pb-2 w-full text-md-0 text-grey no-select pointer d-none"></div>
|
<div class="order-time flex items-center justify-end mt-2 pt-2 pb-2 w-full text-md-0 text-grey no-select pointer d-none"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`
|
</section>`
|
||||||
)
|
)
|
||||||
this.$component = this.wrapper.find('.payment-section');
|
this.$component = this.wrapper.find('.payment-section');
|
||||||
this.$payment_modes = this.$component.find('.payment-modes');
|
this.$payment_modes = this.$component.find('.payment-modes');
|
||||||
this.$totals_remarks = this.$component.find('.totals-remarks');
|
this.$totals_remarks = this.$component.find('.totals-remarks');
|
||||||
this.$totals = this.$component.find('.totals');
|
this.$totals = this.$component.find('.totals');
|
||||||
@ -174,6 +174,24 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
frappe.realtime.on("process_phone_payment", function(data) {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
cur_frm.reload_doc();
|
||||||
|
let message = data["ResultDesc"];
|
||||||
|
let title = __("Payment Failed");
|
||||||
|
|
||||||
|
if (data["ResultCode"] == 0) {
|
||||||
|
title = __("Payment Received");
|
||||||
|
$('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`)
|
||||||
|
me.events.submit_invoice();
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.msgprint({
|
||||||
|
"message": message,
|
||||||
|
"title": title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.$payment_modes.on('click', '.shortcut', function(e) {
|
this.$payment_modes.on('click', '.shortcut', function(e) {
|
||||||
const value = $(this).attr('data-value');
|
const value = $(this).attr('data-value');
|
||||||
me.selected_mode.set_value(value);
|
me.selected_mode.set_value(value);
|
||||||
@ -509,5 +527,5 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
|
|
||||||
toggle_component(show) {
|
toggle_component(show) {
|
||||||
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,10 @@ frappe.ui.form.on("Shopping Cart Settings", {
|
|||||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
||||||
frm.refresh_field("quotation_series");
|
frm.refresh_field("quotation_series");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.set_query('payment_gateway_account', function() {
|
||||||
|
return { 'filters': { 'payment_channel': "Email" } };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
enabled: function(frm) {
|
enabled: function(frm) {
|
||||||
if (frm.doc.enabled === 1) {
|
if (frm.doc.enabled === 1) {
|
||||||
|
@ -57,7 +57,7 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
|
|
||||||
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
|
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
|
||||||
|
|
||||||
self.assertEqual(sle.stock_value_difference, -1*stock_queue[0][1])
|
self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1], 2))
|
||||||
|
|
||||||
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
||||||
|
|
||||||
|
@ -1117,7 +1117,10 @@ class StockEntry(StockController):
|
|||||||
for d in backflushed_materials.get(item.item_code):
|
for d in backflushed_materials.get(item.item_code):
|
||||||
if d.get(item.warehouse):
|
if d.get(item.warehouse):
|
||||||
if (qty > req_qty):
|
if (qty > req_qty):
|
||||||
qty-= d.get(item.warehouse)
|
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
|
||||||
|
|
||||||
|
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
|
||||||
|
qty = frappe.utils.ceil(qty)
|
||||||
|
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.add_to_stock_entry_detail({
|
self.add_to_stock_entry_detail({
|
||||||
@ -1320,8 +1323,10 @@ class StockEntry(StockController):
|
|||||||
for sr in get_serial_nos(item.serial_no):
|
for sr in get_serial_nos(item.serial_no):
|
||||||
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
|
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
|
||||||
if sales_order:
|
if sales_order:
|
||||||
frappe.throw(_("Item {0} (Serial No: {1}) cannot be consumed as is reserverd\
|
msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.")
|
||||||
to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order))
|
.format(sr, sales_order))
|
||||||
|
|
||||||
|
frappe.throw(_("Item {0} {1}").format(item.item_code, msg))
|
||||||
|
|
||||||
def update_transferred_qty(self):
|
def update_transferred_qty(self):
|
||||||
if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
|
if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
"options": "FIFO\nMoving Average"
|
"options": "FIFO\nMoving Average"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Percentage you are allowed to receive or deliver more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to receive 110 units.",
|
"description": "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.",
|
||||||
"fieldname": "over_delivery_receipt_allowance",
|
"fieldname": "over_delivery_receipt_allowance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Over Delivery/Receipt Allowance (%)"
|
"label": "Over Delivery/Receipt Allowance (%)"
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"default": "Stop",
|
"default": "Stop",
|
||||||
"fieldname": "action_if_quality_inspection_is_not_submitted",
|
"fieldname": "action_if_quality_inspection_is_not_submitted",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Action if Quality inspection is not submitted",
|
"label": "Action If Quality Inspection Is Not Submitted",
|
||||||
"options": "Stop\nWarn"
|
"options": "Stop\nWarn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -114,7 +114,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "auto_insert_price_list_rate_if_missing",
|
"fieldname": "auto_insert_price_list_rate_if_missing",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto insert Price List rate if missing"
|
"label": "Auto Insert Price List Rate If Missing"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@ -130,13 +130,13 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "automatically_set_serial_nos_based_on_fifo",
|
"fieldname": "automatically_set_serial_nos_based_on_fifo",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Automatically Set Serial Nos based on FIFO"
|
"label": "Automatically Set Serial Nos Based on FIFO"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "set_qty_in_transactions_based_on_serial_no_input",
|
"fieldname": "set_qty_in_transactions_based_on_serial_no_input",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Set Qty in Transactions based on Serial No Input"
|
"label": "Set Qty in Transactions Based on Serial No Input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "auto_material_request",
|
"fieldname": "auto_material_request",
|
||||||
@ -147,13 +147,13 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "auto_indent",
|
"fieldname": "auto_indent",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Raise Material Request when stock reaches re-order level"
|
"label": "Raise Material Request When Stock Reaches Re-order Level"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "reorder_email_notify",
|
"fieldname": "reorder_email_notify",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Notify by Email on creation of automatic Material Request"
|
"label": "Notify by Email on Creation of Automatic Material Request"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "freeze_stock_entries",
|
"fieldname": "freeze_stock_entries",
|
||||||
@ -168,12 +168,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "stock_frozen_upto_days",
|
"fieldname": "stock_frozen_upto_days",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Freeze Stocks Older Than [Days]"
|
"label": "Freeze Stocks Older Than (Days)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "stock_auth_role",
|
"fieldname": "stock_auth_role",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to edit frozen stock",
|
"label": "Role Allowed to Edit Frozen Stock",
|
||||||
"options": "Role"
|
"options": "Role"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -203,20 +203,21 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_from_dn",
|
"fieldname": "allow_from_dn",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Material Transfer From Delivery Note and Sales Invoice"
|
"label": "Allow Material Transfer from Delivery Note to Sales Invoice"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_from_pr",
|
"fieldname": "allow_from_pr",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Material Transfer From Purchase Receipt and Purchase Invoice"
|
"label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-20 11:39:15.344112",
|
"modified": "2020-10-13 10:33:29.147682",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user