Merge branch 'develop' into debit-credit-opening-invoice-tool
This commit is contained in:
commit
6f45a86920
@ -40,7 +40,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
@ -48,23 +48,23 @@
|
||||
"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",
|
||||
"fieldtype": "Date",
|
||||
"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",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
"label": "Determine Address Tax Category From",
|
||||
@ -75,7 +75,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -127,7 +127,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "show_inclusive_tax_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Inclusive Tax In Print"
|
||||
"label": "Show Inclusive Tax in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@ -165,7 +165,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Custom Cash Flow Format"
|
||||
@ -177,7 +177,7 @@
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Over Billing Allowance (%)"
|
||||
@ -199,7 +199,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Deferred Entries Via Journal Entry"
|
||||
@ -214,7 +214,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
"label": "Book Deferred Entries Based On",
|
||||
@ -226,7 +226,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-07 14:58:50.325577",
|
||||
"modified": "2020-10-13 11:32:52.268826",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -29,7 +29,7 @@ class CashierClosing(Document):
|
||||
for i in self.payments:
|
||||
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):
|
||||
if self.from_time >= self.time:
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:mode_of_payment",
|
||||
@ -28,7 +29,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Cash\nBank\nGeneral"
|
||||
"options": "Cash\nBank\nGeneral\nPhone"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
@ -45,7 +46,9 @@
|
||||
],
|
||||
"icon": "fa fa-credit-card",
|
||||
"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",
|
||||
"module": "Accounts",
|
||||
"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) {
|
||||
return {
|
||||
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) {
|
||||
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) {
|
||||
frm.disable_save();
|
||||
frm.trigger("make_dashboard");
|
||||
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||
frm.page.set_primary_action(__('Create Invoices'), () => {
|
||||
let btn_primary = frm.page.btn_primary.get(0);
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
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();
|
||||
}
|
||||
}
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||
});
|
||||
});
|
||||
|
||||
if (frm.doc.create_missing_party) {
|
||||
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||
}
|
||||
},
|
||||
|
||||
setup_company_filters: function(frm) {
|
||||
|
@ -4,9 +4,12 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import traceback
|
||||
from json import dumps
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import flt, nowdate
|
||||
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
|
||||
|
||||
|
||||
@ -62,66 +65,47 @@ class OpeningInvoiceCreationTool(Document):
|
||||
|
||||
return invoices_summary, max_count
|
||||
|
||||
def make_invoices(self):
|
||||
names = []
|
||||
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
|
||||
def validate_company(self):
|
||||
if not self.company:
|
||||
frappe.throw(_("Please select the Company"))
|
||||
|
||||
company_details = frappe.get_cached_value('Company', self.company,
|
||||
["default_currency", "default_letter_head"], as_dict=1) or {}
|
||||
|
||||
for row in self.invoices:
|
||||
if not row.qty:
|
||||
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)
|
||||
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()
|
||||
|
||||
# Allow to create invoice even if no party present in customer or supplier.
|
||||
def validate_mandatory_invoice_fields(self, row):
|
||||
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()
|
||||
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))
|
||||
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||
|
||||
args = self.get_invoice_dict(row=row)
|
||||
if not args:
|
||||
def get_invoices(self):
|
||||
invoices = []
|
||||
for row in self.invoices:
|
||||
if not row:
|
||||
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:
|
||||
args.update({
|
||||
invoice.update({
|
||||
"currency": company_details.get("default_currency"),
|
||||
"letter_head": company_details.get("default_letter_head")
|
||||
})
|
||||
invoices.append(invoice)
|
||||
|
||||
doc = frappe.get_doc(args).insert()
|
||||
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
|
||||
return invoices
|
||||
|
||||
def add_party(self, party_type, party):
|
||||
party_doc = frappe.new_doc(party_type)
|
||||
@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
|
||||
|
||||
def get_invoice_dict(self, row=None):
|
||||
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:
|
||||
frappe.throw(
|
||||
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
|
||||
)
|
||||
frappe.throw(_("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)
|
||||
|
||||
return frappe._dict({
|
||||
@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"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()
|
||||
|
||||
args = frappe._dict({
|
||||
invoice = frappe._dict({
|
||||
"items": [item],
|
||||
"is_opening": "Yes",
|
||||
"set_posting_time": 1,
|
||||
@ -180,22 +153,77 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"cost_center": self.cost_center,
|
||||
"due_date": row.due_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",
|
||||
"update_stock": 0
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
|
||||
for dimension in accounting_dimension:
|
||||
args.update({
|
||||
invoice.update({
|
||||
dimension: item.get(dimension)
|
||||
})
|
||||
|
||||
if self.invoice_type == "Sales":
|
||||
args["is_pos"] = 0
|
||||
return invoice
|
||||
|
||||
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()
|
||||
def get_temporary_opening_account(company=None):
|
||||
|
@ -44,7 +44,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
0: ["_Test Supplier", 300, "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):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
|
@ -1,313 +1,98 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"creation": "2015-12-23 21:31:52.699821",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"field_order": [
|
||||
"payment_gateway",
|
||||
"payment_channel",
|
||||
"is_default",
|
||||
"column_break_4",
|
||||
"payment_account",
|
||||
"currency",
|
||||
"payment_request_message",
|
||||
"message",
|
||||
"message_examples"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"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_standard_filter": 0,
|
||||
"label": "Payment Gateway",
|
||||
"length": 0,
|
||||
"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
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "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
|
||||
"label": "Is Default"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"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
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"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_standard_filter": 0,
|
||||
"label": "Payment Account",
|
||||
"length": 0,
|
||||
"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
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "payment_account.account_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "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
|
||||
"label": "Currency"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
|
||||
"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
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"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
|
||||
"label": "Default Payment Request Message"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "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
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"default": "Email",
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-16 22:43:34.970491",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-20 13:30:27.722852",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -25,7 +25,7 @@ frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){
|
||||
})
|
||||
|
||||
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){
|
||||
frm.add_custom_button(__('Resend Payment Email'), function(){
|
||||
frappe.call({
|
||||
|
@ -48,6 +48,7 @@
|
||||
"section_break_7",
|
||||
"payment_gateway",
|
||||
"payment_account",
|
||||
"payment_channel",
|
||||
"payment_order",
|
||||
"amended_from"
|
||||
],
|
||||
@ -230,6 +231,7 @@
|
||||
"label": "Recipient Message And Payment Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Print Format"
|
||||
@ -241,6 +243,7 @@
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -277,16 +280,18 @@
|
||||
"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",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
@ -347,12 +352,21 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-17 14:06:42.185763",
|
||||
"modified": "2020-09-18 12:24:14.178853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
@ -36,7 +36,7 @@ class PaymentRequest(Document):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if (hasattr(ref_doc, "order_type") \
|
||||
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:
|
||||
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
||||
@ -76,11 +76,25 @@ class PaymentRequest(Document):
|
||||
or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
if send_mail:
|
||||
if send_mail and self.payment_channel != "Phone":
|
||||
self.set_payment_request_url()
|
||||
self.send_email()
|
||||
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):
|
||||
self.check_if_payment_entry_exists()
|
||||
self.set_as_cancelled()
|
||||
@ -105,13 +119,14 @@ class PaymentRequest(Document):
|
||||
return False
|
||||
|
||||
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()
|
||||
|
||||
if 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')
|
||||
|
||||
def get_payment_url(self):
|
||||
@ -140,6 +155,10 @@ class PaymentRequest(Document):
|
||||
})
|
||||
|
||||
def set_as_paid(self):
|
||||
if self.payment_channel == "Phone":
|
||||
self.db_set("status", "Paid")
|
||||
|
||||
else:
|
||||
payment_entry = self.create_payment_entry()
|
||||
self.make_invoice()
|
||||
|
||||
@ -151,7 +170,7 @@ class PaymentRequest(Document):
|
||||
|
||||
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
|
||||
elif self.reference_doctype == "Purchase Invoice":
|
||||
party_account = ref_doc.credit_to
|
||||
@ -166,8 +185,8 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
|
||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name,
|
||||
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount)
|
||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
|
||||
bank_account=self.payment_account, bank_amount=bank_amount)
|
||||
|
||||
payment_entry.update({
|
||||
"reference_no": self.name,
|
||||
@ -255,7 +274,7 @@ class PaymentRequest(Document):
|
||||
|
||||
# if shopping cart enabled and in 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
|
||||
if success_url:
|
||||
@ -280,7 +299,9 @@ def make_payment_request(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
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":
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_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)
|
||||
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'))
|
||||
if args.get('party_type') else '')
|
||||
|
||||
@ -314,9 +333,11 @@ def make_payment_request(**args):
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
"payment_gateway": gateway_account.get("payment_gateway"),
|
||||
"payment_account": gateway_account.get("payment_account"),
|
||||
"payment_channel": gateway_account.get("payment_channel"),
|
||||
"payment_request_type": args.get("payment_request_type"),
|
||||
"currency": ref_doc.currency,
|
||||
"grand_total": grand_total,
|
||||
"mode_of_payment": args.mode_of_payment,
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
"subject": _("Payment Request for {0}").format(args.dn),
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
@ -344,7 +365,7 @@ def make_payment_request(**args):
|
||||
|
||||
return pr.as_dict()
|
||||
|
||||
def get_amount(ref_doc):
|
||||
def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
@ -356,6 +377,12 @@ def get_amount(ref_doc):
|
||||
else:
|
||||
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":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
@ -366,6 +393,10 @@ def get_amount(ref_doc):
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
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("""
|
||||
select sum(grand_total)
|
||||
from `tabPayment Request`
|
||||
@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
reference_doctype = %s
|
||||
and reference_name = %s
|
||||
and docstatus = 1
|
||||
and status != 'Paid'
|
||||
and (status != 'Paid'
|
||||
or (payment_channel = 'Phone'
|
||||
and status = 'Paid'))
|
||||
""", (ref_dt, ref_dn))
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
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",
|
||||
"label": "Is Return (Credit Note)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"set_only_once": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
@ -461,7 +460,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"read_only": 1
|
||||
@ -1579,10 +1578,9 @@
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-07 12:43:09.138720",
|
||||
"modified": "2020-09-28 16:51:24.641755",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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.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
|
||||
|
||||
@ -57,6 +58,7 @@ class POSInvoice(SalesInvoice):
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
if self.redeem_loyalty_points and self.loyalty_points:
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
|
||||
def on_cancel(self):
|
||||
@ -69,6 +71,18 @@ class POSInvoice(SalesInvoice):
|
||||
against_psi_doc.delete_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):
|
||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||
|
||||
@ -312,6 +326,32 @@ class POSInvoice(SalesInvoice):
|
||||
if not pay.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()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||
|
@ -243,7 +243,11 @@ def check_amount_vs_description(amount_matching, description_matching):
|
||||
continue
|
||||
|
||||
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:
|
||||
result.append(am_match)
|
||||
if result:
|
||||
|
@ -796,7 +796,7 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
|
||||
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
|
||||
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
@ -831,7 +831,8 @@ def create_payment_gateway_account(gateway):
|
||||
"is_default": 1,
|
||||
"payment_gateway": gateway,
|
||||
"payment_account": bank_account.name,
|
||||
"currency": bank_account.account_currency
|
||||
"currency": bank_account.account_currency,
|
||||
"payment_channel": payment_channel
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
|
@ -46,26 +46,26 @@
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purchase Order Required for Purchase Invoice & Receipt Creation",
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purchase Receipt Required for Purchase Invoice Creation",
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain same rate throughout purchase cycle"
|
||||
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_multiple_items",
|
||||
"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",
|
||||
@ -93,9 +93,10 @@
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-15 14:49:32.513611",
|
||||
"modified": "2020-10-13 12:00:23.276329",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -29,14 +29,12 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
|
||||
refresh: function(frm, cdt, cdn) {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frm.add_custom_button(__('Create'),
|
||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Supplier Quotation"));
|
||||
|
||||
frm.add_custom_button(__("View"),
|
||||
function(){ frappe.set_route('List', 'Supplier Quotation',
|
||||
{'request_for_quotation': frm.doc.name}) }, __("Supplier Quotation"));
|
||||
frm.add_custom_button(__('Supplier Quotation'),
|
||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
||||
|
||||
frm.add_custom_button(__("Send Supplier Emails"), function() {
|
||||
|
||||
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
||||
freeze: true,
|
||||
@ -47,134 +45,64 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, __("Tools"));
|
||||
|
||||
},
|
||||
|
||||
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"
|
||||
},
|
||||
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;})]
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
dialog.fields_dict.add_suppliers.$input.click(function() {
|
||||
var args = dialog.get_values();
|
||||
if(!args) return;
|
||||
dialog.hide();
|
||||
frappe.prompt(fields, data => {
|
||||
var child = locals[cdt][cdn]
|
||||
|
||||
//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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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]]
|
||||
'Download PDF for Supplier',
|
||||
'Download');
|
||||
},
|
||||
__("Tools"));
|
||||
|
||||
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
},
|
||||
callback: load_suppliers
|
||||
});
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
|
||||
},
|
||||
make_suppplier_quotation: function(frm) {
|
||||
var doc = frm.doc;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("For Supplier"),
|
||||
title: __("Create Supplier Quotation"),
|
||||
fields: [
|
||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
||||
"fieldname": "supplier",
|
||||
"options": doc.suppliers.map(d => d.supplier),
|
||||
"reqd": 1,
|
||||
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
||||
{ "fieldtype": "Button", "label": __('Create Supplier Quotation'),
|
||||
"fieldname": "make_supplier_quotation", "cssClass": "btn-primary" },
|
||||
]
|
||||
});
|
||||
|
||||
dialog.fields_dict.make_supplier_quotation.$input.click(function() {
|
||||
var args = dialog.get_values();
|
||||
],
|
||||
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",
|
||||
@ -190,7 +118,9 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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({
|
||||
@ -332,7 +226,8 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
per_ordered: ["<", 99.99]
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
}, __("Get Items From"));
|
||||
|
||||
// Get items from Opportunity
|
||||
this.frm.add_custom_button(__('Opportunity'),
|
||||
function() {
|
||||
@ -344,7 +239,8 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
company: me.frm.doc.company
|
||||
},
|
||||
})
|
||||
}, __("Get items from"));
|
||||
}, __("Get Items From"));
|
||||
|
||||
// Get items from open Material Requests based on supplier
|
||||
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
||||
// Create a dialog window for the user to pick their supplier
|
||||
@ -382,8 +278,13 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
}
|
||||
}
|
||||
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() {
|
||||
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
|
||||
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
||||
|
@ -12,9 +12,10 @@
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"status",
|
||||
"amended_from",
|
||||
"suppliers_section",
|
||||
"suppliers",
|
||||
"get_suppliers_button",
|
||||
"items_section",
|
||||
"items",
|
||||
"link_to_mrs",
|
||||
@ -31,11 +32,7 @@
|
||||
"terms",
|
||||
"printing_settings",
|
||||
"select_print_heading",
|
||||
"letter_head",
|
||||
"more_info",
|
||||
"status",
|
||||
"column_break3",
|
||||
"amended_from"
|
||||
"letter_head"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -83,6 +80,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
@ -99,16 +97,11 @@
|
||||
{
|
||||
"fieldname": "suppliers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplier Detail",
|
||||
"label": "Suppliers",
|
||||
"options": "Request for Quotation Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "get_suppliers_button",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Suppliers"
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -144,6 +137,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "email_template.response",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "message_for_supplier",
|
||||
@ -206,14 +200,6 @@
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@ -227,10 +213,6 @@
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@ -275,9 +257,10 @@
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-01 14:54:50.888729",
|
||||
"modified": "2020-10-16 17:49:09.561929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
@ -28,6 +28,10 @@ class RequestforQuotation(BuyingController):
|
||||
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
||||
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):
|
||||
supplier_list = [d.supplier for d in self.suppliers]
|
||||
if len(supplier_list) != len(set(supplier_list)):
|
||||
@ -82,7 +86,7 @@ class RequestforQuotation(BuyingController):
|
||||
# make new user if required
|
||||
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())
|
||||
rfq_supplier.email_sent = 1
|
||||
if not rfq_supplier.contact:
|
||||
@ -93,11 +97,11 @@ class RequestforQuotation(BuyingController):
|
||||
# RFQ link for supplier portal
|
||||
return get_url("/rfq/" + self.name)
|
||||
|
||||
def update_supplier_part_no(self, args):
|
||||
self.vendor = args.supplier
|
||||
def update_supplier_part_no(self, supplier):
|
||||
self.vendor = supplier
|
||||
for item in self.items:
|
||||
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):
|
||||
'''Create a new user for the supplier if not set in contact'''
|
||||
@ -197,7 +201,6 @@ class RequestforQuotation(BuyingController):
|
||||
def update_rfq_supplier_status(self, sup_name=None):
|
||||
for supplier in self.suppliers:
|
||||
if sup_name == None or supplier.supplier == sup_name:
|
||||
if supplier.quote_status != _('No Quote'):
|
||||
quote_status = _('Received')
|
||||
for item in self.items:
|
||||
sqi_count = frappe.db.sql("""
|
||||
@ -322,16 +325,15 @@ def create_rfq_items(sq_doc, supplier, data):
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier_idx):
|
||||
doc = get_rfq_doc(doctype, name, supplier_idx)
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
if doc:
|
||||
download_pdf(doctype, name, doc=doc)
|
||||
|
||||
def get_rfq_doc(doctype, name, supplier_idx):
|
||||
if cint(supplier_idx):
|
||||
def get_rfq_doc(doctype, name, supplier):
|
||||
if supplier:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
args = doc.get('suppliers')[cint(supplier_idx) - 1]
|
||||
doc.update_supplier_part_no(args)
|
||||
doc.update_supplier_part_no(supplier)
|
||||
return doc
|
||||
|
||||
@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.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)
|
||||
|
||||
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):
|
||||
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();
|
||||
},
|
||||
() => frappe.timeout(1),
|
||||
() => {
|
||||
frappe.click_check('No Quote');
|
||||
},
|
||||
() => frappe.timeout(1),
|
||||
() => {
|
||||
cur_frm.cur_grid.toggle_view();
|
||||
@ -125,7 +122,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
||||
() => 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[0].doc.no_quote == 1);
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
@ -27,10 +27,11 @@
|
||||
"stock_qty",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
"project_name",
|
||||
"col_break4",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"section_break_24",
|
||||
"project_name",
|
||||
"section_break_23",
|
||||
"page_break"
|
||||
],
|
||||
@ -161,7 +162,7 @@
|
||||
{
|
||||
"fieldname": "project_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project Name",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
},
|
||||
@ -249,11 +250,18 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-12 19:10:36.333441",
|
||||
"modified": "2020-09-24 17:26:46.276934",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Item",
|
||||
|
@ -9,19 +9,19 @@
|
||||
"email_sent",
|
||||
"supplier",
|
||||
"contact",
|
||||
"no_quote",
|
||||
"quote_status",
|
||||
"column_break_3",
|
||||
"supplier_name",
|
||||
"email_id",
|
||||
"download_pdf"
|
||||
"email_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"columns": 2,
|
||||
"default": "1",
|
||||
"fieldname": "send_email",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Send Email"
|
||||
},
|
||||
{
|
||||
@ -35,7 +35,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"columns": 2,
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -45,7 +45,7 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "contact",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -55,19 +55,11 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"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",
|
||||
"depends_on": "eval:doc.docstatus >= 1",
|
||||
"fieldname": "quote_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Quote Status",
|
||||
"options": "Pending\nReceived\nNo Quote",
|
||||
"options": "Pending\nReceived",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -90,17 +82,12 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Email Id",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "download_pdf",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download PDF"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-28 19:31:11.855588",
|
||||
"modified": "2020-10-16 12:23:41.769820",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Supplier",
|
||||
|
@ -91,12 +91,7 @@ class SupplierQuotation(BuyingController):
|
||||
for my_item in self.items) if include_me else 0
|
||||
if (sqi_count.count + self_count) == 0:
|
||||
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)
|
||||
|
||||
def get_list_context(context=None):
|
||||
|
@ -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 _
|
||||
import base64, hashlib, hmac
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from erpnext import get_default_company
|
||||
|
||||
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
||||
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)
|
||||
|
||||
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)
|
@ -181,8 +181,11 @@ class Employee(NestedSet):
|
||||
)
|
||||
if 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: ")
|
||||
+ ', '.join(link_to_employees), EmployeeLeftValidationError)
|
||||
message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name))
|
||||
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:
|
||||
throw(_("Please enter relieving date."))
|
||||
|
||||
@ -215,7 +218,7 @@ class Employee(NestedSet):
|
||||
|
||||
def validate_preferred_email(self):
|
||||
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):
|
||||
employee_onboarding = frappe.get_all("Employee Onboarding",
|
||||
@ -417,9 +420,9 @@ def get_employee_emails(employee_list):
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
|
||||
|
||||
filters = []
|
||||
filters = [['status', '!=', 'Left']]
|
||||
if company and company != 'All Companies':
|
||||
filters = [['company', '=', company]]
|
||||
filters.append(['company', '=', company])
|
||||
|
||||
fields = ['name as value', 'employee_name as title']
|
||||
|
||||
|
@ -28,144 +28,110 @@
|
||||
{
|
||||
"fieldname": "employee_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Employee Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Employee Settings"
|
||||
},
|
||||
{
|
||||
"description": "Enter retirement age in years",
|
||||
"fieldname": "retirement_age",
|
||||
"fieldtype": "Data",
|
||||
"label": "Retirement Age",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Retirement Age"
|
||||
},
|
||||
{
|
||||
"default": "Naming Series",
|
||||
"description": "Employee record is created using selected field. ",
|
||||
"description": "Employee records are created using the selected field",
|
||||
"fieldname": "emp_created_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Employee Records to be created by",
|
||||
"options": "Naming Series\nEmployee Number\nFull Name",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Employee Records to Be Created By",
|
||||
"options": "Naming Series\nEmployee Number\nFull Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't send Employee Birthday Reminders",
|
||||
"description": "Don't send employee birthday reminders",
|
||||
"fieldname": "stop_birthday_reminders",
|
||||
"fieldtype": "Check",
|
||||
"label": "Stop Birthday Reminders",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Stop Birthday Reminders"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "expense_approver_mandatory_in_expense_claim",
|
||||
"fieldtype": "Check",
|
||||
"label": "Expense Approver Mandatory In Expense Claim",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Expense Approver Mandatory In Expense Claim"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "leave_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Leave Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Leave Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "leave_approval_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Approval Notification Template",
|
||||
"options": "Email Template",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "leave_status_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Status Notification Template",
|
||||
"options": "Email Template",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "leave_approver_mandatory_in_leave_application",
|
||||
"fieldtype": "Check",
|
||||
"label": "Leave Approver Mandatory In Leave Application",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Leave Approver Mandatory In Leave Application"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_leaves_of_all_department_members_in_calendar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Leaves Of All Department Members In Calendar",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Show Leaves Of All Department Members In Calendar"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "hiring_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Hiring Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Hiring Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "check_vacancies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Vacancies On Job Offer Creation",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Check Vacancies On Job Offer Creation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "auto_leave_encashment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Leave Encashment",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Auto Leave Encashment"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "restrict_backdated_leave_application",
|
||||
"fieldtype": "Check",
|
||||
"label": "Restrict Backdated Leave Application",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Restrict Backdated Leave Applications"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
|
||||
"fieldname": "role_allowed_to_create_backdated_leave_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Create Backdated Leave Application",
|
||||
"options": "Role",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-04 15:15:09.865476",
|
||||
"modified": "2020-10-13 11:49:46.168027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR Settings",
|
||||
|
@ -55,10 +55,11 @@ class BOM(WebsiteGenerator):
|
||||
conflicting_bom = frappe.get_doc("BOM", name)
|
||||
|
||||
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}.
|
||||
<br> Did you rename the item? Please contact Administrator / Tech support
|
||||
""").format(frappe.bold(name), frappe.bold(conflicting_bom.item)))
|
||||
frappe.throw(_("{0}{1} Did you rename the item? Please contact Administrator / Tech support")
|
||||
.format(msg, "<br>"))
|
||||
|
||||
self.name = name
|
||||
|
||||
@ -72,6 +73,7 @@ class BOM(WebsiteGenerator):
|
||||
self.validate_uom_is_interger()
|
||||
self.set_bom_material_details()
|
||||
self.validate_materials()
|
||||
self.set_routing_operations()
|
||||
self.validate_operations()
|
||||
self.calculate_cost()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
||||
@ -111,18 +113,13 @@ class BOM(WebsiteGenerator):
|
||||
def get_routing(self):
|
||||
if self.routing:
|
||||
self.set("operations", [])
|
||||
for d in frappe.get_all("BOM Operation", fields = ["*"],
|
||||
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="idx"):
|
||||
child = self.append('operations', {
|
||||
"operation": d.operation,
|
||||
"workstation": d.workstation,
|
||||
"description": d.description,
|
||||
"time_in_mins": d.time_in_mins,
|
||||
"batch_size": d.batch_size,
|
||||
"operating_cost": d.operating_cost,
|
||||
"idx": d.idx
|
||||
})
|
||||
child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2)
|
||||
fields = ["sequence_id", "operation", "workstation", "description",
|
||||
"time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"]
|
||||
|
||||
for row in frappe.get_all("BOM Operation", fields = fields,
|
||||
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
|
||||
child = self.append('operations', row)
|
||||
child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2)
|
||||
|
||||
def set_bom_material_details(self):
|
||||
for item in self.get("items"):
|
||||
@ -571,6 +568,10 @@ class BOM(WebsiteGenerator):
|
||||
if act_pbom and act_pbom[0][0]:
|
||||
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):
|
||||
if self.with_operations and not self.get('operations'):
|
||||
frappe.throw(_("Operations cannot be left blank"))
|
||||
|
@ -1,10 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2013-02-22 01:27:49",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sequence_id",
|
||||
"operation",
|
||||
"workstation",
|
||||
"description",
|
||||
@ -106,11 +108,19 @@
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.parenttype == \"Routing\"",
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"label": "Sequence ID"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"modified": "2020-06-16 17:01:11.128420",
|
||||
"links": [],
|
||||
"modified": "2020-10-13 18:14:10.018774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"items",
|
||||
"more_information",
|
||||
"operation_id",
|
||||
"sequence_id",
|
||||
"transferred_qty",
|
||||
"requested_qty",
|
||||
"column_break_20",
|
||||
@ -297,10 +298,18 @@
|
||||
"fieldname": "operation_row_number",
|
||||
"fieldtype": "Select",
|
||||
"label": "Operation Row Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"label": "Sequence Id",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-08-24 15:21:21.398267",
|
||||
"links": [],
|
||||
"modified": "2020-10-14 12:58:25.327897",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
@ -5,7 +5,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import datetime
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.document import Document
|
||||
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 OperationMismatchError(frappe.ValidationError): pass
|
||||
class OperationSequenceError(frappe.ValidationError): pass
|
||||
|
||||
class JobCard(Document):
|
||||
def validate(self):
|
||||
self.validate_time_logs()
|
||||
self.set_status()
|
||||
self.validate_operation_id()
|
||||
self.validate_sequence_id()
|
||||
|
||||
def validate_time_logs(self):
|
||||
self.total_completed_qty = 0.0
|
||||
@ -196,14 +198,14 @@ class JobCard(Document):
|
||||
def validate_job_card(self):
|
||||
if not self.time_logs:
|
||||
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:
|
||||
total_completed_qty = frappe.bold(_("Total Completed Qty"))
|
||||
qty_to_manufacture = frappe.bold(_("Qty to Manufacture"))
|
||||
total_completed_qty = bold(_("Total Completed Qty"))
|
||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||
|
||||
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))))
|
||||
frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})")
|
||||
.format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity)))
|
||||
|
||||
def update_work_order(self):
|
||||
if not self.work_order:
|
||||
@ -213,10 +215,7 @@ class JobCard(Document):
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
field = "operation_id"
|
||||
data = 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, field: self.get(field)})
|
||||
|
||||
data = self.get_current_operation_data()
|
||||
if data and len(data) > 0:
|
||||
for_quantity = data[0].completed_qty
|
||||
time_in_mins = data[0].time_in_mins
|
||||
@ -246,6 +245,11 @@ class JobCard(Document):
|
||||
wo.set_actual_dates()
|
||||
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):
|
||||
if not self.items:
|
||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||
@ -310,9 +314,32 @@ class JobCard(Document):
|
||||
def validate_operation_id(self):
|
||||
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):
|
||||
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}")
|
||||
.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()
|
||||
def get_operation_details(work_order, operation):
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2014-11-27 14:12:07.542534",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
@ -36,7 +37,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Overtime"
|
||||
@ -56,17 +57,17 @@
|
||||
{
|
||||
"default": "30",
|
||||
"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",
|
||||
"fieldtype": "Int",
|
||||
"label": "Capacity Planning For (Days)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_capacity_planning",
|
||||
"description": "Default 10 mins",
|
||||
"description": "Default: 10 mins",
|
||||
"fieldname": "mins_between_operations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Time Between Operations (in mins)"
|
||||
"label": "Time Between Operations (Mins)"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
@ -92,14 +93,14 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allow multiple Material Consumption against a Work Order",
|
||||
"description": "Allow multiple material consumptions against a Work Order",
|
||||
"fieldname": "material_consumption",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Multiple Material Consumption"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update BOM Cost Automatically"
|
||||
@ -135,7 +136,7 @@
|
||||
{
|
||||
"fieldname": "over_production_for_sales_and_work_order_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Over Production for Sales and Work Order"
|
||||
"label": "Overproduction for Sales and Work Order"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_materials_consumption_section",
|
||||
@ -157,8 +158,10 @@
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-11-26 13:10:45.569341",
|
||||
"links": [],
|
||||
"modified": "2020-10-13 10:55:43.996581",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
@ -9,3 +9,23 @@ test_records = frappe.get_test_records('Operation')
|
||||
|
||||
class TestOperation(unittest.TestCase):
|
||||
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,
|
||||
'currency': args.currency or 'USD',
|
||||
'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:
|
||||
|
@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
@ -3,7 +3,22 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
from frappe.model.document import 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
|
||||
|
||||
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):
|
||||
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),
|
||||
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):
|
||||
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||
target= self.warehouse, qty=100, basic_rate=100)
|
||||
|
@ -378,7 +378,7 @@ class WorkOrder(Document):
|
||||
select
|
||||
operation, description, workstation, idx,
|
||||
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
|
||||
`tabBOM Operation`
|
||||
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,
|
||||
'project': work_order.project,
|
||||
'company': work_order.company,
|
||||
'sequence_id': row.get("sequence_id"),
|
||||
'wip_warehouse': work_order.wip_warehouse
|
||||
})
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
"details",
|
||||
"operation",
|
||||
"bom",
|
||||
"sequence_id",
|
||||
"description",
|
||||
"col_break1",
|
||||
"completed_qty",
|
||||
@ -187,11 +188,19 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2019-12-03 19:24:29.594189",
|
||||
"modified": "2020-10-14 12:58:49.241252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
@ -21,17 +21,22 @@ class TestWorkstation(unittest.TestCase):
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
workstation_name = args.workstation_name or args.workstation
|
||||
try:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": args.workstation_name
|
||||
"workstation_name": workstation_name
|
||||
})
|
||||
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Workstation", args.workstation_name)
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
@ -730,3 +730,4 @@ erpnext.patches.v13_0.rename_issue_doctype_fields
|
||||
erpnext.patches.v13_0.change_default_pos_print_format
|
||||
erpnext.patches.v13_0.set_youtube_video_id
|
||||
erpnext.patches.v13_0.print_uom_after_quantity_patch
|
||||
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
|
||||
|
@ -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
|
||||
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
|
||||
@ -554,19 +554,32 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
},
|
||||
|
||||
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 dialog = new frappe.ui.Dialog({
|
||||
title: __("For Supplier"),
|
||||
title: __("Select Items"),
|
||||
fields: [
|
||||
{"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier",
|
||||
"description": __("Leave the field empty to make purchase orders for all suppliers"),
|
||||
"get_query": function () {
|
||||
return {
|
||||
query:"erpnext.selling.doctype.sales_order.sales_order.get_supplier",
|
||||
filters: {'parent': me.frm.doc.name}
|
||||
}
|
||||
}},
|
||||
{fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
||||
{
|
||||
"fieldtype": "Check",
|
||||
"label": __("Against Default Supplier"),
|
||||
"fieldname": "against_default_supplier",
|
||||
"default": 0
|
||||
},
|
||||
{
|
||||
"fieldtype": "Section Break",
|
||||
"label": "",
|
||||
"fieldname": "sec_break_dialog",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
||||
fields: [
|
||||
{
|
||||
fieldtype:'Data',
|
||||
@ -584,8 +597,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
},
|
||||
{
|
||||
fieldtype:'Float',
|
||||
fieldname:'qty',
|
||||
label: __('Quantity'),
|
||||
fieldname:'pending_qty',
|
||||
label: __('Pending Qty'),
|
||||
read_only: 1,
|
||||
in_list_view:1
|
||||
},
|
||||
@ -594,47 +607,48 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
read_only:1,
|
||||
fieldname:'uom',
|
||||
label: __('UOM'),
|
||||
in_list_view:1,
|
||||
},
|
||||
{
|
||||
fieldtype:'Data',
|
||||
fieldname:'supplier',
|
||||
label: __('Supplier'),
|
||||
read_only:1,
|
||||
in_list_view:1
|
||||
},
|
||||
],
|
||||
data: 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;})
|
||||
}
|
||||
],
|
||||
data: cur_frm.doc.items,
|
||||
get_data: function() {
|
||||
return cur_frm.doc.items
|
||||
}
|
||||
},
|
||||
|
||||
{"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()
|
||||
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 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)
|
||||
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.make_purchase_order",
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order." + method,
|
||||
args: {
|
||||
"source_name": me.frm.doc.name,
|
||||
"for_supplier": args.supplier,
|
||||
"selected_items": selected_items_list
|
||||
"selected_items": selected_items
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
// var args = dialog.get_values();
|
||||
if (args.supplier){
|
||||
var doc = frappe.model.sync(r.message);
|
||||
if (!args.against_default_supplier) {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
else{
|
||||
else {
|
||||
frappe.route_options = {
|
||||
"sales_order": me.frm.doc.name
|
||||
}
|
||||
@ -643,11 +657,36 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
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();
|
||||
},
|
||||
|
||||
hold_sales_order: function(){
|
||||
var me = this;
|
||||
var d = new frappe.ui.Dialog({
|
||||
|
@ -443,25 +443,19 @@ class SalesOrder(SellingController):
|
||||
for item in self.items:
|
||||
if item.ensure_delivery_based_on_produced_serial_no:
|
||||
if item.item_code in normal_items:
|
||||
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))
|
||||
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))
|
||||
if item.item_code not in reserved_items:
|
||||
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
||||
frappe.throw(_("Item {0} has no Serial No. Only serilialized items \
|
||||
can have delivery based on Serial No").format(item.item_code))
|
||||
frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code))
|
||||
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
||||
frappe.throw(_("No active BOM found for item {0}. Delivery by \
|
||||
Serial No cannot be ensured").format(item.item_code))
|
||||
frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code))
|
||||
reserved_items.append(item.item_code)
|
||||
else:
|
||||
normal_items.append(item.item_code)
|
||||
|
||||
if not item.ensure_delivery_based_on_produced_serial_no and \
|
||||
item.item_code in reserved_items:
|
||||
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))
|
||||
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))
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
@ -785,7 +779,7 @@ def get_events(start, end, filters=None):
|
||||
return data
|
||||
|
||||
@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):
|
||||
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):
|
||||
target.schedule_date = source.delivery_date
|
||||
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
||||
target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor)
|
||||
target.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
|
||||
|
||||
suppliers =[]
|
||||
if for_supplier:
|
||||
suppliers.append(for_supplier)
|
||||
else:
|
||||
sales_order = frappe.get_doc("Sales Order", source_name)
|
||||
for item in sales_order.items:
|
||||
if item.supplier and item.supplier not in suppliers:
|
||||
suppliers.append(item.supplier)
|
||||
suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
|
||||
suppliers = list(set(suppliers))
|
||||
|
||||
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||
items_to_map = list(set(items_to_map))
|
||||
|
||||
if not suppliers:
|
||||
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
|
||||
|
||||
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:
|
||||
doc = get_mapped_doc("Sales Order", source_name, {
|
||||
"Sales Order": {
|
||||
@ -850,7 +841,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges"
|
||||
"taxes_and_charges",
|
||||
"shipping_address"
|
||||
],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1]
|
||||
@ -872,52 +864,82 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
||||
"item_tax_template"
|
||||
],
|
||||
"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)
|
||||
if not for_supplier:
|
||||
|
||||
doc.insert()
|
||||
else:
|
||||
suppliers =[]
|
||||
if suppliers:
|
||||
if not for_supplier:
|
||||
frappe.db.commit()
|
||||
return doc
|
||||
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.validate_and_sanitize_search_inputs
|
||||
def get_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
if supp_master_name == "Supplier Name":
|
||||
fields = ["name", "supplier_group"]
|
||||
else:
|
||||
fields = ["name", "supplier_name", "supplier_group"]
|
||||
fields = ", ".join(fields)
|
||||
def make_purchase_order(source_name, selected_items=[], target_doc=None):
|
||||
if isinstance(selected_items, string_types):
|
||||
selected_items = json.loads(selected_items)
|
||||
|
||||
return frappe.db.sql("""select {field} from `tabSupplier`
|
||||
where docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or supplier_name like %(txt)s)
|
||||
and name in (select supplier from `tabSales Order Item` where parent = %(parent)s)
|
||||
and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi
|
||||
on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s)
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
||||
name, supplier_name
|
||||
limit %(start)s, %(page_len)s """.format(**{
|
||||
'field': fields,
|
||||
'key': frappe.db.escape(searchfield)
|
||||
}), {
|
||||
'txt': "%%%s%%" % txt,
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len,
|
||||
'parent': filters.get('parent')
|
||||
})
|
||||
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||
items_to_map = list(set(items_to_map))
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.supplier = ""
|
||||
target.apply_discount_on = ""
|
||||
target.additional_discount_percentage = 0.0
|
||||
target.discount_amount = 0.0
|
||||
target.inter_company_order_reference = ""
|
||||
target.customer = ""
|
||||
target.customer_name = ""
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.schedule_date = source.delivery_date
|
||||
target.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
|
||||
|
||||
# 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()
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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})
|
||||
|
||||
dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
|
||||
|
||||
so_items = [
|
||||
@ -715,80 +715,61 @@ class TestSalesOrder(unittest.TestCase):
|
||||
]
|
||||
|
||||
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
|
||||
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
|
||||
#create so, po and dn
|
||||
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
||||
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()
|
||||
|
||||
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(po.items[0].sales_order, so.name)
|
||||
self.assertEqual(po.items[0].item_code, po_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
|
||||
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)
|
||||
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
|
||||
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
|
||||
# test after closing so
|
||||
so.db_set('status', "Closed")
|
||||
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"])
|
||||
|
||||
ordered_qty = bin[0].ordered_qty if bin else 0.0
|
||||
reserved_qty = bin[0].reserved_qty if bin else 0.0
|
||||
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
|
||||
|
||||
self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty)
|
||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty)
|
||||
self.assertEqual(abs(flt(ordered_qty)), 0)
|
||||
self.assertEqual(abs(flt(reserved_qty)), 0)
|
||||
|
||||
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)
|
||||
# teardown
|
||||
so_update_status("Draft", so.name)
|
||||
dn.load_from_db()
|
||||
dn.cancel()
|
||||
po.cancel()
|
||||
so.load_from_db()
|
||||
so.cancel()
|
||||
|
||||
def test_reserved_qty_for_closing_so(self):
|
||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||
|
@ -63,7 +63,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Int",
|
||||
"label": "Close Opportunity After Days"
|
||||
@ -80,18 +80,18 @@
|
||||
{
|
||||
"fieldname": "so_required",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldname": "dn_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Delivery Note Required for Sales Invoice Creation",
|
||||
"label": "Is Delivery Note Required for Sales Invoice Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
"label": "Sales Update Frequency",
|
||||
@ -108,38 +108,39 @@
|
||||
"default": "0",
|
||||
"fieldname": "editable_price_list_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow user to edit Price List Rate in transactions"
|
||||
"label": "Allow User to Edit Price List Rate in Transactions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_multiple_items",
|
||||
"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",
|
||||
"fieldname": "allow_against_multiple_purchase_orders",
|
||||
"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",
|
||||
"fieldname": "validate_selling_price",
|
||||
"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",
|
||||
"fieldname": "hide_tax_id",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Customer's Tax Id from Sales Transactions"
|
||||
"label": "Hide Customer's Tax ID from Sales Transactions"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-01 13:58:35.637858",
|
||||
"modified": "2020-10-13 12:12:56.784014",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
@ -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) {
|
||||
const value = $(this).attr('data-value');
|
||||
me.selected_mode.set_value(value);
|
||||
|
@ -7,6 +7,10 @@ frappe.ui.form.on("Shopping Cart Settings", {
|
||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
||||
frm.refresh_field("quotation_series");
|
||||
}
|
||||
|
||||
frm.set_query('payment_gateway_account', function() {
|
||||
return { 'filters': { 'payment_channel': "Email" } };
|
||||
});
|
||||
},
|
||||
enabled: function(frm) {
|
||||
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})
|
||||
|
||||
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))
|
||||
|
||||
|
@ -1117,7 +1117,10 @@ class StockEntry(StockController):
|
||||
for d in backflushed_materials.get(item.item_code):
|
||||
if d.get(item.warehouse):
|
||||
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:
|
||||
self.add_to_stock_entry_detail({
|
||||
@ -1320,8 +1323,10 @@ class StockEntry(StockController):
|
||||
for sr in get_serial_nos(item.serial_no):
|
||||
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
|
||||
if sales_order:
|
||||
frappe.throw(_("Item {0} (Serial No: {1}) cannot be consumed as is reserverd\
|
||||
to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order))
|
||||
msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.")
|
||||
.format(sr, sales_order))
|
||||
|
||||
frappe.throw(_("Item {0} {1}").format(item.item_code, msg))
|
||||
|
||||
def update_transferred_qty(self):
|
||||
if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
|
||||
|
@ -82,7 +82,7 @@
|
||||
"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",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Delivery/Receipt Allowance (%)"
|
||||
@ -91,7 +91,7 @@
|
||||
"default": "Stop",
|
||||
"fieldname": "action_if_quality_inspection_is_not_submitted",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Quality inspection is not submitted",
|
||||
"label": "Action If Quality Inspection Is Not Submitted",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
@ -114,7 +114,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "auto_insert_price_list_rate_if_missing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto insert Price List rate if missing"
|
||||
"label": "Auto Insert Price List Rate If Missing"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -130,13 +130,13 @@
|
||||
"default": "1",
|
||||
"fieldname": "automatically_set_serial_nos_based_on_fifo",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Set Serial Nos based on FIFO"
|
||||
"label": "Automatically Set Serial Nos Based on FIFO"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "set_qty_in_transactions_based_on_serial_no_input",
|
||||
"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",
|
||||
@ -147,13 +147,13 @@
|
||||
"default": "0",
|
||||
"fieldname": "auto_indent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Raise Material Request when stock reaches re-order level"
|
||||
"label": "Raise Material Request When Stock Reaches Re-order Level"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reorder_email_notify",
|
||||
"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",
|
||||
@ -168,12 +168,12 @@
|
||||
{
|
||||
"fieldname": "stock_frozen_upto_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Freeze Stocks Older Than [Days]"
|
||||
"label": "Freeze Stocks Older Than (Days)"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_auth_role",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to edit frozen stock",
|
||||
"label": "Role Allowed to Edit Frozen Stock",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
@ -203,20 +203,21 @@
|
||||
"default": "0",
|
||||
"fieldname": "allow_from_dn",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Material Transfer From Delivery Note and Sales Invoice"
|
||||
"label": "Allow Material Transfer from Delivery Note to Sales Invoice"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_from_pr",
|
||||
"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",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-20 11:39:15.344112",
|
||||
"modified": "2020-10-13 10:33:29.147682",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
Loading…
x
Reference in New Issue
Block a user