Merge branch 'develop' into feat-manufacturer-is-missing-contacts

This commit is contained in:
Saurabh
2019-07-12 11:10:12 +05:30
committed by GitHub
83 changed files with 1019 additions and 3208 deletions

View File

@@ -9,6 +9,26 @@ frappe.ui.form.on('Accounting Dimension', {
frappe.set_route("List", frm.doc.document_type); frappe.set_route("List", frm.doc.document_type);
}); });
} }
let button = frm.doc.disabled ? "Enable" : "Disable";
frm.add_custom_button(__(button), function() {
frm.set_value('disabled', 1 - frm.doc.disabled);
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.disable_dimension",
args: {
doc: frm.doc
},
freeze: true,
callback: function(r) {
let message = frm.doc.disabled ? "Dimension Disabled" : "Dimension Enabled";
frm.save();
frappe.show_alert({message:__(message), indicator:'green'});
}
});
});
}, },
document_type: function(frm) { document_type: function(frm) {
@@ -21,13 +41,4 @@ frappe.ui.form.on('Accounting Dimension', {
} }
}); });
}, },
disabled: function(frm) {
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.disable_dimension",
args: {
doc: frm.doc
}
});
}
}); });

View File

@@ -38,7 +38,8 @@
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable" "label": "Disable",
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@@ -53,7 +54,7 @@
"label": "Mandatory For Profit and Loss Account" "label": "Mandatory For Profit and Loss Account"
} }
], ],
"modified": "2019-05-27 18:18:17.792726", "modified": "2019-07-07 18:56:19.517450",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension", "name": "Accounting Dimension",

View File

@@ -121,11 +121,11 @@ def delete_accounting_dimension(doc):
@frappe.whitelist() @frappe.whitelist()
def disable_dimension(doc): def disable_dimension(doc):
if frappe.flags.in_test: if frappe.flags.in_test:
frappe.enqueue(start_dimension_disabling, doc=doc) toggle_disabling(doc=doc)
else: else:
start_dimension_disabling(doc=doc) frappe.enqueue(toggle_disabling, doc=doc)
def start_dimension_disabling(doc): def toggle_disabling(doc):
doc = json.loads(doc) doc = json.loads(doc)
if doc.get('disabled'): if doc.get('disabled'):

View File

@@ -103,7 +103,7 @@ class BankReconciliation(Document):
for d in self.get('payment_entries'): for d in self.get('payment_entries'):
if d.clearance_date: if d.clearance_date:
if not d.payment_document: if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the trasaction")) frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}") frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}")
@@ -113,10 +113,8 @@ class BankReconciliation(Document):
if not d.clearance_date: if not d.clearance_date:
d.clearance_date = None d.clearance_date = None
frappe.db.set_value(d.payment_document, d.payment_entry, "clearance_date", d.clearance_date) payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
frappe.db.sql("""update `tab{0}` set clearance_date = %s, modified = %s payment_entry.db_set('clearance_date', d.clearance_date)
where name=%s""".format(d.payment_document),
(d.clearance_date, nowdate(), d.payment_entry))
clearance_date_updated = True clearance_date_updated = True

View File

@@ -21,9 +21,29 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
refresh: function(frm) { refresh: function(frm) {
if(frm.doc.docstatus==1) { if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Create Journal Entry'), function() { frappe.db.get_value("Journal Entry Account", {
return frm.events.make_jv(frm); 'reference_type': 'Exchange Rate Revaluation',
}); 'reference_name': frm.doc.name,
'docstatus': 1
}, "sum(debit) as sum", (r) =>{
let total_amt = 0;
frm.doc.accounts.forEach(d=> {
total_amt = total_amt + d['new_balance_in_base_currency'];
});
if(total_amt === r.sum) {
frm.add_custom_button(__("Journal Entry"), function(){
frappe.route_options = {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': frm.doc.name
};
frappe.set_route("List", "Journal Entry");
}, __("View"));
} else {
frm.add_custom_button(__('Create Journal Entry'), function() {
return frm.events.make_jv(frm);
});
}
}, 'Journal Entry');
} }
}, },

View File

@@ -848,6 +848,39 @@
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "due_date",
"fieldtype": "Date",
"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": "Due Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "has_web_view": 0,
@@ -861,7 +894,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2019-01-07 07:05:00.366399", "modified": "2019-05-01 07:05:00.366399",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GL Entry", "name": "GL Entry",

View File

@@ -498,6 +498,7 @@ class JournalEntry(AccountsController):
self.get_gl_dict({ self.get_gl_dict({
"account": d.account, "account": d.account,
"party_type": d.party_type, "party_type": d.party_type,
"due_date": self.due_date,
"party": d.party, "party": d.party,
"against": d.against_account, "against": d.against_account,
"debit": flt(d.debit, d.precision("debit")), "debit": flt(d.debit, d.precision("debit")),

View File

@@ -302,7 +302,7 @@ frappe.ui.form.on('Payment Entry', {
}, },
() => frm.set_value("party_balance", r.message.party_balance), () => frm.set_value("party_balance", r.message.party_balance),
() => frm.set_value("party_name", r.message.party_name), () => frm.set_value("party_name", r.message.party_name),
() => frm.events.get_outstanding_documents(frm), () => frm.clear_table("references"),
() => frm.events.hide_unhide_fields(frm), () => frm.events.hide_unhide_fields(frm),
() => frm.events.set_dynamic_labels(frm), () => frm.events.set_dynamic_labels(frm),
() => { () => {
@@ -323,9 +323,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_account_currency_and_balance(frm, frm.doc.paid_from, frm.events.set_account_currency_and_balance(frm, frm.doc.paid_from,
"paid_from_account_currency", "paid_from_account_balance", function(frm) { "paid_from_account_currency", "paid_from_account_balance", function(frm) {
if (frm.doc.payment_type == "Receive") { if (frm.doc.payment_type == "Pay") {
frm.events.get_outstanding_documents(frm);
} else if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm); frm.events.paid_amount(frm);
} }
} }
@@ -337,9 +335,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_account_currency_and_balance(frm, frm.doc.paid_to, frm.events.set_account_currency_and_balance(frm, frm.doc.paid_to,
"paid_to_account_currency", "paid_to_account_balance", function(frm) { "paid_to_account_currency", "paid_to_account_balance", function(frm) {
if(frm.doc.payment_type == "Pay") { if (frm.doc.payment_type == "Receive") {
frm.events.get_outstanding_documents(frm);
} else if (frm.doc.payment_type == "Receive") {
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
if(frm.doc.source_exchange_rate) { if(frm.doc.source_exchange_rate) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
@@ -533,26 +529,87 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_unallocated_amount(frm); frm.events.set_unallocated_amount(frm);
}, },
get_outstanding_documents: function(frm) { get_outstanding_invoice: function(frm) {
const today = frappe.datetime.get_today();
const fields = [
{fieldtype:"Section Break", label: __("Posting Date")},
{fieldtype:"Date", label: __("From Date"),
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
{fieldtype:"Column Break"},
{fieldtype:"Date", label: __("To Date"), fieldname:"to_posting_date", default:today},
{fieldtype:"Section Break", label: __("Due Date")},
{fieldtype:"Date", label: __("From Date"), fieldname:"from_due_date"},
{fieldtype:"Column Break"},
{fieldtype:"Date", label: __("To Date"), fieldname:"to_due_date"},
{fieldtype:"Section Break", label: __("Outstanding Amount")},
{fieldtype:"Float", label: __("Greater Than Amount"),
fieldname:"outstanding_amt_greater_than", default: 0},
{fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
];
frappe.prompt(fields, function(filters){
frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters);
frm.events.get_outstanding_documents(frm, filters);
}, __("Filters"), __("Get Outstanding Invoices"));
},
validate_filters_data: function(frm, filters) {
const fields = {
'Posting Date': ['from_posting_date', 'to_posting_date'],
'Due Date': ['from_posting_date', 'to_posting_date'],
'Advance Amount': ['from_posting_date', 'to_posting_date'],
};
for (let key in fields) {
let from_field = fields[key][0];
let to_field = fields[key][1];
if (filters[from_field] && !filters[to_field]) {
frappe.throw(__("Error: {0} is mandatory field",
[to_field.replace(/_/g, " ")]
));
} else if (filters[from_field] && filters[from_field] > filters[to_field]) {
frappe.throw(__("{0}: {1} must be less than {2}",
[key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")]
));
}
}
},
get_outstanding_documents: function(frm, filters) {
frm.clear_table("references"); frm.clear_table("references");
if(!frm.doc.party) return; if(!frm.doc.party) {
return;
}
frm.events.check_mandatory_to_fetch(frm); frm.events.check_mandatory_to_fetch(frm);
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
var args = {
"posting_date": frm.doc.posting_date,
"company": frm.doc.company,
"party_type": frm.doc.party_type,
"payment_type": frm.doc.payment_type,
"party": frm.doc.party,
"party_account": frm.doc.payment_type=="Receive" ? frm.doc.paid_from : frm.doc.paid_to,
"cost_center": frm.doc.cost_center
}
for (let key in filters) {
args[key] = filters[key];
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({ return frappe.call({
method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_outstanding_reference_documents', method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_outstanding_reference_documents',
args: { args: {
args: { args:args
"posting_date": frm.doc.posting_date,
"company": frm.doc.company,
"party_type": frm.doc.party_type,
"payment_type": frm.doc.payment_type,
"party": frm.doc.party,
"party_account": frm.doc.payment_type=="Receive" ? frm.doc.paid_from : frm.doc.paid_to,
"cost_center": frm.doc.cost_center
}
}, },
callback: function(r, rt) { callback: function(r, rt) {
if(r.message) { if(r.message) {
@@ -608,25 +665,11 @@ frappe.ui.form.on('Payment Entry', {
frm.events.allocate_party_amount_against_ref_docs(frm, frm.events.allocate_party_amount_against_ref_docs(frm,
(frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount)); (frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount));
} }
}); });
}, },
allocate_payment_amount: function(frm) {
if(frm.doc.payment_type == 'Internal Transfer'){
return
}
if(frm.doc.references.length == 0){
frm.events.get_outstanding_documents(frm);
}
if(frm.doc.payment_type == 'Internal Transfer') {
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount);
} else {
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount);
}
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount) { allocate_party_amount_against_ref_docs: function(frm, paid_amount) {
var total_positive_outstanding_including_order = 0; var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0; var total_negative_outstanding = 0;
@@ -677,7 +720,7 @@ frappe.ui.form.on('Payment Entry', {
$.each(frm.doc.references || [], function(i, row) { $.each(frm.doc.references || [], function(i, row) {
row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount
if(frm.doc.allocate_payment_amount){ if(frappe.flags.allocate_payment_amount){
if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if(row.outstanding_amount >= allocated_positive_outstanding) { if(row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding; row.allocated_amount = allocated_positive_outstanding;
@@ -958,7 +1001,7 @@ frappe.ui.form.on('Payment Entry', {
}, },
() => { () => {
if(frm.doc.payment_type != "Internal") { if(frm.doc.payment_type != "Internal") {
frm.events.get_outstanding_documents(frm); frm.clear_table("references");
} }
} }
]); ]);

View File

@@ -40,7 +40,7 @@
"target_exchange_rate", "target_exchange_rate",
"base_received_amount", "base_received_amount",
"section_break_14", "section_break_14",
"allocate_payment_amount", "get_outstanding_invoice",
"references", "references",
"section_break_34", "section_break_34",
"total_allocated_amount", "total_allocated_amount",
@@ -325,19 +325,15 @@
"reqd": 1 "reqd": 1
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "references",
"depends_on": "eval:(doc.party && doc.paid_from && doc.paid_to && doc.paid_amount && doc.received_amount)", "depends_on": "eval:(doc.party && doc.paid_from && doc.paid_to && doc.paid_amount && doc.received_amount)",
"fieldname": "section_break_14", "fieldname": "section_break_14",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Reference" "label": "Reference"
}, },
{ {
"default": "1", "fieldname": "get_outstanding_invoice",
"depends_on": "eval:in_list(['Pay', 'Receive'], doc.payment_type)", "fieldtype": "Button",
"fieldname": "allocate_payment_amount", "label": "Get Outstanding Invoice"
"fieldtype": "Check",
"label": "Allocate Payment Amount"
}, },
{ {
"fieldname": "references", "fieldname": "references",
@@ -570,7 +566,7 @@
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-05-25 22:02:40.575822", "modified": "2019-05-27 15:53:21.108857",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -574,8 +574,8 @@ def get_outstanding_reference_documents(args):
# Get negative outstanding sales /purchase invoices # Get negative outstanding sales /purchase invoices
negative_outstanding_invoices = [] negative_outstanding_invoices = []
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"): if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
args.get("party"), args.get("party_account"), party_account_currency, company_currency) args.get("party_account"), args.get("company"), party_account_currency, company_currency)
# Get positive outstanding sales /purchase invoices/ Fees # Get positive outstanding sales /purchase invoices/ Fees
condition = "" condition = ""
@@ -585,10 +585,23 @@ def get_outstanding_reference_documents(args):
# Add cost center condition # Add cost center condition
if args.get("cost_center") and get_allow_cost_center_in_entry_of_bs_account(): if args.get("cost_center") and get_allow_cost_center_in_entry_of_bs_account():
condition += " and cost_center='%s'" % args.get("cost_center") condition += " and cost_center='%s'" % args.get("cost_center")
date_fields_dict = {
'posting_date': ['from_posting_date', 'to_posting_date'],
'due_date': ['from_due_date', 'to_due_date']
}
for fieldname, date_fields in date_fields_dict.items():
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += " and {0} between '{1}' and '{2}'".format(fieldname,
args.get(date_fields[0]), args.get(date_fields[1]))
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"), outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
args.get("party_account"), condition=condition) args.get("party_account"), filters=args, condition=condition, limit=100)
for d in outstanding_invoices: for d in outstanding_invoices:
d["exchange_rate"] = 1 d["exchange_rate"] = 1
@@ -606,12 +619,19 @@ def get_outstanding_reference_documents(args):
orders_to_be_billed = [] orders_to_be_billed = []
if (args.get("party_type") != "Student"): if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
args.get("party"), party_account_currency, company_currency) args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
return negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} <b>{1}</b>.")
.format(args.get("party_type").lower(), args.get("party")))
return data
def get_orders_to_be_billed(posting_date, party_type, party, party_account_currency, company_currency, cost_center=None): def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None):
if party_type == "Customer": if party_type == "Customer":
voucher_type = 'Sales Order' voucher_type = 'Sales Order'
elif party_type == "Supplier": elif party_type == "Supplier":
@@ -641,6 +661,7 @@ def get_orders_to_be_billed(posting_date, party_type, party, party_account_curre
where where
{party_type} = %s {party_type} = %s
and docstatus = 1 and docstatus = 1
and company = %s
and ifnull(status, "") != "Closed" and ifnull(status, "") != "Closed"
and {ref_field} > advance_paid and {ref_field} > advance_paid
and abs(100 - per_billed) > 0.01 and abs(100 - per_billed) > 0.01
@@ -652,10 +673,14 @@ def get_orders_to_be_billed(posting_date, party_type, party, party_account_curre
"voucher_type": voucher_type, "voucher_type": voucher_type,
"party_type": scrub(party_type), "party_type": scrub(party_type),
"condition": condition "condition": condition
}), party, as_dict=True) }), (party, company), as_dict=True)
order_list = [] order_list = []
for d in orders: for d in orders:
if not (d.outstanding_amount >= filters.get("outstanding_amt_greater_than")
and d.outstanding_amount <= filters.get("outstanding_amt_less_than")):
continue
d["voucher_type"] = voucher_type d["voucher_type"] = voucher_type
# This assumes that the exchange rate required is the one in the SO # This assumes that the exchange rate required is the one in the SO
d["exchange_rate"] = get_exchange_rate(party_account_currency, company_currency, posting_date) d["exchange_rate"] = get_exchange_rate(party_account_currency, company_currency, posting_date)
@@ -663,7 +688,8 @@ def get_orders_to_be_billed(posting_date, party_type, party, party_account_curre
return order_list return order_list
def get_negative_outstanding_invoices(party_type, party, party_account, party_account_currency, company_currency, cost_center=None): def get_negative_outstanding_invoices(party_type, party, party_account,
company, party_account_currency, company_currency, cost_center=None):
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = "" supplier_condition = ""
if voucher_type == "Purchase Invoice": if voucher_type == "Purchase Invoice":
@@ -684,7 +710,8 @@ def get_negative_outstanding_invoices(party_type, party, party_account, party_ac
from from
`tab{voucher_type}` `tab{voucher_type}`
where where
{party_type} = %s and {party_account} = %s and docstatus = 1 and outstanding_amount < 0 {party_type} = %s and {party_account} = %s and docstatus = 1 and
company = %s and outstanding_amount < 0
{supplier_condition} {supplier_condition}
order by order by
posting_date, name posting_date, name
@@ -696,7 +723,7 @@ def get_negative_outstanding_invoices(party_type, party, party_account, party_ac
"party_type": scrub(party_type), "party_type": scrub(party_type),
"party_account": "debit_to" if party_type == "Customer" else "credit_to", "party_account": "debit_to" if party_type == "Customer" else "credit_to",
"cost_center": cost_center "cost_center": cost_center
}), (party, party_account), as_dict=True) }), (party, party_account, company), as_dict=True)
@frappe.whitelist() @frappe.whitelist()
@@ -924,7 +951,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.paid_to_account_currency = party_account_currency if payment_type=="Pay" else bank.account_currency pe.paid_to_account_currency = party_account_currency if payment_type=="Pay" else bank.account_currency
pe.paid_amount = paid_amount pe.paid_amount = paid_amount
pe.received_amount = received_amount pe.received_amount = received_amount
pe.allocate_payment_amount = 1
pe.letter_head = doc.get("letter_head") pe.letter_head = doc.get("letter_head")
if pe.party_type in ["Customer", "Supplier"]: if pe.party_type in ["Customer", "Supplier"]:

View File

@@ -337,7 +337,8 @@ class PurchaseInvoice(BuyingController):
if not self.is_return: if not self.is_return:
self.update_against_document_in_jv() self.update_against_document_in_jv()
self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr()
self.update_billing_status_in_pr()
# Updating stock ledger should always be called after updating prevdoc status, # Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO
@@ -416,6 +417,7 @@ class PurchaseInvoice(BuyingController):
"account": self.credit_to, "account": self.credit_to,
"party_type": "Supplier", "party_type": "Supplier",
"party": self.supplier, "party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account, "against": self.against_expense_account,
"credit": grand_total_in_company_currency, "credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \ "credit_in_account_currency": grand_total_in_company_currency \
@@ -773,7 +775,8 @@ class PurchaseInvoice(BuyingController):
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr()
self.update_billing_status_in_pr()
# Updating stock ledger should always be called after updating prevdoc status, # Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO

View File

@@ -187,9 +187,13 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
method: "erpnext.selling.doctype.quotation.quotation.make_sales_invoice", method: "erpnext.selling.doctype.quotation.quotation.make_sales_invoice",
source_doctype: "Quotation", source_doctype: "Quotation",
target: me.frm, target: me.frm,
setters: { setters: [{
customer: me.frm.doc.customer || undefined, fieldtype: 'Link',
}, label: __('Customer'),
options: 'Customer',
fieldname: 'party_name',
default: me.frm.doc.customer,
}],
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
status: ["!=", "Lost"], status: ["!=", "Lost"],

View File

@@ -734,6 +734,7 @@ class SalesInvoice(SellingController):
"account": self.debit_to, "account": self.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account, "against": self.against_income_account,
"debit": grand_total_in_company_currency, "debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \ "debit_in_account_currency": grand_total_in_company_currency \

View File

@@ -108,3 +108,14 @@ frappe.query_reports["Accounts Payable"] = {
}); });
} }
} }
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Accounts Payable"].filters.splice(9, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});

View File

@@ -92,3 +92,14 @@ frappe.query_reports["Accounts Payable Summary"] = {
}); });
} }
} }
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Accounts Payable Summary"].filters.splice(9, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});

View File

@@ -172,3 +172,14 @@ frappe.query_reports["Accounts Receivable"] = {
}); });
} }
} }
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Accounts Receivable"].filters.splice(9, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _, scrub from frappe import _, scrub
from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class ReceivablePayableReport(object): class ReceivablePayableReport(object):
def __init__(self, filters=None): def __init__(self, filters=None):
@@ -553,6 +554,14 @@ class ReceivablePayableReport(object):
conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts)))
values += accounts values += accounts
accounting_dimensions = get_accounting_dimensions()
if accounting_dimensions:
for dimension in accounting_dimensions:
if self.filters.get(dimension):
conditions.append("{0} = %s".format(dimension))
values.append(self.filters.get(dimension))
return " and ".join(conditions), values return " and ".join(conditions), values
def get_gl_entries_for(self, party, party_type, against_voucher_type, against_voucher): def get_gl_entries_for(self, party, party_type, against_voucher_type, against_voucher):

View File

@@ -116,3 +116,14 @@ frappe.query_reports["Accounts Receivable Summary"] = {
}); });
} }
} }
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Accounts Receivable Summary"].filters.splice(9, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});

View File

@@ -63,9 +63,7 @@ frappe.query_reports["Budget Variance Report"] = {
] ]
} }
let dimension_filters = erpnext.get_dimension_filters(); erpnext.dimension_filters.then((dimensions) => {
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => { dimensions.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
}); });

View File

@@ -8,17 +8,19 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
// The last item in the array is the definition for Presentation Currency // The last item in the array is the definition for Presentation Currency
// filter. It won't be used in cash flow for now so we pop it. Please take // filter. It won't be used in cash flow for now so we pop it. Please take
// of this if you are working here. // of this if you are working here.
frappe.query_reports["Cash Flow"]["filters"].pop();
frappe.query_reports["Cash Flow"]["filters"].push({ frappe.query_reports["Cash Flow"]["filters"].splice(5, 1);
"fieldname": "accumulated_values",
"label": __("Accumulated Values"),
"fieldtype": "Check"
});
frappe.query_reports["Cash Flow"]["filters"].push({ frappe.query_reports["Cash Flow"]["filters"].push(
"fieldname": "include_default_book_entries", {
"label": __("Include Default Book Entries"), "fieldname": "accumulated_values",
"fieldtype": "Check" "label": __("Accumulated Values"),
}); "fieldtype": "Check"
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"fieldtype": "Check"
}
);
}); });

View File

@@ -159,9 +159,7 @@ frappe.query_reports["General Ledger"] = {
] ]
} }
let dimension_filters = erpnext.get_dimension_filters(); erpnext.dimension_filters.then((dimensions) => {
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => { dimensions.forEach((dimension) => {
frappe.query_reports["General Ledger"].filters.splice(15, 0 ,{ frappe.query_reports["General Ledger"].filters.splice(15, 0 ,{
"fieldname": dimension["fieldname"], "fieldname": dimension["fieldname"],

View File

@@ -93,4 +93,6 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss):
else: else:
chart["type"] = "line" chart["type"] = "line"
chart["fieldtype"] = "Currency"
return chart return chart

View File

@@ -16,7 +16,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "based_on", "fieldname": "based_on",
"label": __("Based On"), "label": __("Based On"),
"fieldtype": "Select", "fieldtype": "Select",
"options": "Cost Center\nProject", "options": ["Cost Center", "Project"],
"default": "Cost Center", "default": "Cost Center",
"reqd": 1 "reqd": 1
}, },
@@ -104,5 +104,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"parent_field": "parent_account", "parent_field": "parent_account",
"initial_depth": 3 "initial_depth": 3
} }
});
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
});
});
});

View File

@@ -24,8 +24,17 @@ def get_accounts_data(based_on, company):
if based_on == 'cost_center': if based_on == 'cost_center':
return frappe.db.sql("""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt return frappe.db.sql("""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
from `tabCost Center` where company=%s order by name""", company, as_dict=True) from `tabCost Center` where company=%s order by name""", company, as_dict=True)
else: elif based_on == 'project':
return frappe.get_all('Project', fields = ["name"], filters = {'company': company}, order_by = 'name') return frappe.get_all('Project', fields = ["name"], filters = {'company': company}, order_by = 'name')
else:
filters = {}
doctype = frappe.unscrub(based_on)
has_company = frappe.db.has_column(doctype, 'company')
if has_company:
filters.update({'company': company})
return frappe.get_all(doctype, fields = ["name"], filters = filters, order_by = 'name')
def get_data(accounts, filters, based_on): def get_data(accounts, filters, based_on):
if not accounts: if not accounts:
@@ -42,7 +51,7 @@ def get_data(accounts, filters, based_on):
accumulate_values_into_parents(accounts, accounts_by_name) accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, total_row, parent_children_map, based_on) data = prepare_data(accounts, filters, total_row, parent_children_map, based_on)
data = filter_out_zero_value_rows(data, parent_children_map, data = filter_out_zero_value_rows(data, parent_children_map,
show_zero_values=filters.get("show_zero_values")) show_zero_values=filters.get("show_zero_values"))
return data return data
@@ -112,14 +121,14 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
for key in value_fields: for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3) row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= 0.005: if abs(row[key]) >= 0.005:
# ignore zero values # ignore zero values
has_value = True has_value = True
row["has_value"] = has_value row["has_value"] = has_value
data.append(row) data.append(row)
data.extend([{},total_row]) data.extend([{},total_row])
return data return data
@@ -174,7 +183,7 @@ def set_gl_entries_by_account(company, from_date, to_date, based_on, gl_entries_
if from_date: if from_date:
additional_conditions.append("and posting_date >= %(from_date)s") additional_conditions.append("and posting_date >= %(from_date)s")
gl_entries = frappe.db.sql("""select posting_date, {based_on} as based_on, debit, credit, gl_entries = frappe.db.sql("""select posting_date, {based_on} as based_on, debit, credit,
is_opening, (select root_type from `tabAccount` where name = account) as type is_opening, (select root_type from `tabAccount` where name = account) as type
from `tabGL Entry` where company=%(company)s from `tabGL Entry` where company=%(company)s
{additional_conditions} {additional_conditions}

View File

@@ -67,3 +67,14 @@ frappe.query_reports["Sales Register"] = {
} }
] ]
} }
erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frappe.query_reports["Sales Register"].filters.splice(7, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
def execute(filters=None): def execute(filters=None):
return _execute(filters) return _execute(filters)
@@ -163,6 +164,16 @@ def get_conditions(filters):
where parent=`tabSales Invoice`.name where parent=`tabSales Invoice`.name
and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)""" and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
accounting_dimensions = get_accounting_dimensions()
if accounting_dimensions:
for dimension in accounting_dimensions:
if filters.get(dimension):
conditions += """ and exists(select name from `tabSales Invoice Item`
where parent=`tabSales Invoice`.name
and ifnull(`tabSales Invoice Item`.{0}, '') = %({0})s)""".format(dimension)
return conditions return conditions
def get_invoices(filters, additional_query_columns): def get_invoices(filters, additional_query_columns):

View File

@@ -96,9 +96,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
} }
}); });
let dimension_filters = erpnext.get_dimension_filters(); erpnext.dimension_filters.then((dimensions) => {
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => { dimensions.forEach((dimension) => {
frappe.query_reports["Trial Balance"].filters.splice(5, 0 ,{ frappe.query_reports["Trial Balance"].filters.splice(5, 0 ,{
"fieldname": dimension["fieldname"], "fieldname": dimension["fieldname"],

View File

@@ -628,7 +628,7 @@ def get_held_invoices(party_type, party):
return held_invoices return held_invoices
def get_outstanding_invoices(party_type, party, account, condition=None): def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
outstanding_invoices = [] outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
@@ -644,7 +644,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
invoice_list = frappe.db.sql(""" invoice_list = frappe.db.sql("""
select select
voucher_no, voucher_type, posting_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount voucher_no, voucher_type, posting_date, due_date,
ifnull(sum({dr_or_cr}), 0) as invoice_amount
from from
`tabGL Entry` `tabGL Entry`
where where
@@ -677,7 +678,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
""".format(payment_dr_or_cr=payment_dr_or_cr), { """.format(payment_dr_or_cr=payment_dr_or_cr), {
"party_type": party_type, "party_type": party_type,
"party": party, "party": party,
"account": account, "account": account
}, as_dict=True) }, as_dict=True)
pe_map = frappe._dict() pe_map = frappe._dict()
@@ -688,10 +689,12 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
outstanding_amount = flt(d.invoice_amount - payment_amount, precision) outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
if outstanding_amount > 0.5 / (10**precision): if outstanding_amount > 0.5 / (10**precision):
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices: if (filters.get("outstanding_amt_greater_than") and
due_date = frappe.db.get_value( not (outstanding_amount >= filters.get("outstanding_amt_greater_than") and
d.voucher_type, d.voucher_no, "posting_date" if party_type == "Employee" else "due_date") outstanding_amount <= filters.get("outstanding_amt_less_than"))):
continue
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
outstanding_invoices.append( outstanding_invoices.append(
frappe._dict({ frappe._dict({
'voucher_no': d.voucher_no, 'voucher_no': d.voucher_no,
@@ -700,7 +703,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
'invoice_amount': flt(d.invoice_amount), 'invoice_amount': flt(d.invoice_amount),
'payment_amount': payment_amount, 'payment_amount': payment_amount,
'outstanding_amount': outstanding_amount, 'outstanding_amount': outstanding_amount,
'due_date': due_date 'due_date': d.due_date
}) })
) )

View File

@@ -281,9 +281,9 @@ def get_data():
}, },
{ {
"type": "report", "type": "report",
"is_query_report": True,
"name": "Item Shortage Report", "name": "Item Shortage Report",
"route": "#Report/Bin/Item Shortage Report", "doctype": "Bin"
"doctype": "Purchase Receipt"
}, },
{ {
"type": "report", "type": "report",

View File

@@ -97,4 +97,15 @@ def get_data():
}, },
] ]
}, },
{
"label": _("Settings"),
"icon": "fa fa-list",
"items": [
{
"type": "doctype",
"name": "Support Settings",
"label": _("Support Settings"),
},
]
},
] ]

View File

@@ -206,10 +206,11 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, name idx desc, name
limit %(start)s, %(page_len)s """.format( limit %(start)s, %(page_len)s """.format(
fcond=get_filters_cond(doctype, filters, conditions), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype),
key=searchfield), { key=frappe.db.escape(searchfield)),
'txt': '%' + txt + '%', {
'txt': "%"+frappe.db.escape(txt)+"%",
'_txt': txt.replace("%", ""), '_txt': txt.replace("%", ""),
'start': start or 0, 'start': start or 0,
'page_len': page_len or 20 'page_len': page_len or 20

View File

@@ -75,7 +75,7 @@ def validate_returned_items(doc):
items_returned = False items_returned = False
for d in doc.get("items"): for d in doc.get("items"):
if flt(d.qty) < 0 or d.get('received_qty') < 0: if d.item_code and (flt(d.qty) < 0 or d.get('received_qty') < 0):
if d.item_code not in valid_items: if d.item_code not in valid_items:
frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}")
.format(d.idx, d.item_code, doc.doctype, doc.return_against)) .format(d.idx, d.item_code, doc.doctype, doc.return_against))
@@ -107,6 +107,9 @@ def validate_returned_items(doc):
items_returned = True items_returned = True
elif d.item_name:
items_returned = True
if not items_returned: if not items_returned:
frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))

View File

@@ -294,7 +294,7 @@ class StatusUpdater(Document):
frappe.db.sql("""update `tab%(target_parent_dt)s` frappe.db.sql("""update `tab%(target_parent_dt)s`
set %(target_parent_field)s = round( set %(target_parent_field)s = round(
ifnull((select ifnull((select
ifnull(sum(if(%(target_ref_field)s > %(target_field)s, abs(%(target_field)s), abs(%(target_ref_field)s))), 0) ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
/ sum(abs(%(target_ref_field)s)) * 100 / sum(abs(%(target_ref_field)s)) * 100
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6) from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
%(update_modified)s %(update_modified)s

View File

@@ -7,7 +7,9 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
import json, requests import json
import requests
import traceback
from erpnext import encode_company_abbr from erpnext import encode_company_abbr
# QuickBooks requires a redirect URL, User will be redirect to this URL # QuickBooks requires a redirect URL, User will be redirect to this URL
@@ -32,7 +34,6 @@ def callback(*args, **kwargs):
class QuickBooksMigrator(Document): class QuickBooksMigrator(Document):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(QuickBooksMigrator, self).__init__(*args, **kwargs) super(QuickBooksMigrator, self).__init__(*args, **kwargs)
from pprint import pprint
self.oauth = OAuth2Session( self.oauth = OAuth2Session(
client_id=self.client_id, client_id=self.client_id,
redirect_uri=self.redirect_url, redirect_uri=self.redirect_url,
@@ -46,7 +47,9 @@ class QuickBooksMigrator(Document):
if self.company: if self.company:
# We need a Cost Center corresponding to the selected erpnext Company # We need a Cost Center corresponding to the selected erpnext Company
self.default_cost_center = frappe.db.get_value('Company', self.company, 'cost_center') self.default_cost_center = frappe.db.get_value('Company', self.company, 'cost_center')
self.default_warehouse = frappe.get_all('Warehouse', filters={"company": self.company, "is_group": 0})[0]["name"] company_warehouses = frappe.get_all('Warehouse', filters={"company": self.company, "is_group": 0})
if company_warehouses:
self.default_warehouse = company_warehouses[0].name
if self.authorization_endpoint: if self.authorization_endpoint:
self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
@@ -218,7 +221,7 @@ class QuickBooksMigrator(Document):
def _fetch_general_ledger(self): def _fetch_general_ledger(self):
try: try:
query_uri = "{}/company/{}/reports/GeneralLedger".format(self.api_endpoint ,self.quickbooks_company_id) query_uri = "{}/company/{}/reports/GeneralLedger".format(self.api_endpoint, self.quickbooks_company_id)
response = self._get(query_uri, response = self._get(query_uri,
params={ params={
"columns": ",".join(["tx_date", "txn_type", "credit_amt", "debt_amt"]), "columns": ",".join(["tx_date", "txn_type", "credit_amt", "debt_amt"]),
@@ -493,17 +496,17 @@ class QuickBooksMigrator(Document):
"account_currency": customer["CurrencyRef"]["value"], "account_currency": customer["CurrencyRef"]["value"],
"company": self.company, "company": self.company,
})[0]["name"] })[0]["name"]
except Exception as e: except Exception:
receivable_account = None receivable_account = None
erpcustomer = frappe.get_doc({ erpcustomer = frappe.get_doc({
"doctype": "Customer", "doctype": "Customer",
"quickbooks_id": customer["Id"], "quickbooks_id": customer["Id"],
"customer_name" : encode_company_abbr(customer["DisplayName"], self.company), "customer_name": encode_company_abbr(customer["DisplayName"], self.company),
"customer_type" : "Individual", "customer_type": "Individual",
"customer_group" : "Commercial", "customer_group": "Commercial",
"default_currency": customer["CurrencyRef"]["value"], "default_currency": customer["CurrencyRef"]["value"],
"accounts": [{"company": self.company, "account": receivable_account}], "accounts": [{"company": self.company, "account": receivable_account}],
"territory" : "All Territories", "territory": "All Territories",
"company": self.company, "company": self.company,
}).insert() }).insert()
if "BillAddr" in customer: if "BillAddr" in customer:
@@ -521,7 +524,7 @@ class QuickBooksMigrator(Document):
item_dict = { item_dict = {
"doctype": "Item", "doctype": "Item",
"quickbooks_id": item["Id"], "quickbooks_id": item["Id"],
"item_code" : encode_company_abbr(item["Name"], self.company), "item_code": encode_company_abbr(item["Name"], self.company),
"stock_uom": "Unit", "stock_uom": "Unit",
"is_stock_item": 0, "is_stock_item": 0,
"item_group": "All Item Groups", "item_group": "All Item Groups",
@@ -549,14 +552,14 @@ class QuickBooksMigrator(Document):
erpsupplier = frappe.get_doc({ erpsupplier = frappe.get_doc({
"doctype": "Supplier", "doctype": "Supplier",
"quickbooks_id": vendor["Id"], "quickbooks_id": vendor["Id"],
"supplier_name" : encode_company_abbr(vendor["DisplayName"], self.company), "supplier_name": encode_company_abbr(vendor["DisplayName"], self.company),
"supplier_group" : "All Supplier Groups", "supplier_group": "All Supplier Groups",
"company": self.company, "company": self.company,
}).insert() }).insert()
if "BillAddr" in vendor: if "BillAddr" in vendor:
self._create_address(erpsupplier, "Supplier", vendor["BillAddr"], "Billing") self._create_address(erpsupplier, "Supplier", vendor["BillAddr"], "Billing")
if "ShipAddr" in vendor: if "ShipAddr" in vendor:
self._create_address(erpsupplier, "Supplier",vendor["ShipAddr"], "Shipping") self._create_address(erpsupplier, "Supplier", vendor["ShipAddr"], "Shipping")
except Exception as e: except Exception as e:
self._log_error(e) self._log_error(e)
@@ -829,7 +832,7 @@ class QuickBooksMigrator(Document):
"currency": invoice["CurrencyRef"]["value"], "currency": invoice["CurrencyRef"]["value"],
"conversion_rate": invoice.get("ExchangeRate", 1), "conversion_rate": invoice.get("ExchangeRate", 1),
"posting_date": invoice["TxnDate"], "posting_date": invoice["TxnDate"],
"due_date": invoice.get("DueDate", invoice["TxnDate"]), "due_date": invoice.get("DueDate", invoice["TxnDate"]),
"credit_to": credit_to_account, "credit_to": credit_to_account,
"supplier": frappe.get_all("Supplier", "supplier": frappe.get_all("Supplier",
filters={ filters={
@@ -1200,7 +1203,7 @@ class QuickBooksMigrator(Document):
def _create_address(self, entity, doctype, address, address_type): def _create_address(self, entity, doctype, address, address_type):
try : try:
if not frappe.db.exists({"doctype": "Address", "quickbooks_id": address["Id"]}): if not frappe.db.exists({"doctype": "Address", "quickbooks_id": address["Id"]}):
frappe.get_doc({ frappe.get_doc({
"doctype": "Address", "doctype": "Address",
@@ -1252,8 +1255,6 @@ class QuickBooksMigrator(Document):
def _log_error(self, execption, data=""): def _log_error(self, execption, data=""):
import json, traceback
traceback.print_exc()
frappe.log_error(title="QuickBooks Migration Error", frappe.log_error(title="QuickBooks Migration Error",
message="\n".join([ message="\n".join([
"Data", "Data",

View File

@@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math
from frappe import _
from frappe.utils import flt, rounded
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.hr.doctype.employee_loan.employee_loan import get_monthly_repayment_amount, check_repayment_method
class EmployeeLoanApplication(Document):
def validate(self):
check_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods)
self.validate_loan_amount()
self.get_repayment_details()
def validate_loan_amount(self):
maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
def get_repayment_details(self):
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate:
monthly_interest_amount = self.loan_amount * monthly_interest_rate
if monthly_interest_amount >= self.repayment_amount:
frappe.throw(_("Repayment amount {} should be greater than monthly interest amount {}").
format(self.repayment_amount, monthly_interest_amount))
self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - (monthly_interest_amount))) /
(math.log(1 + monthly_interest_rate)))
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest
@frappe.whitelist()
def make_employee_loan(source_name, target_doc = None):
doclist = get_mapped_doc("Employee Loan Application", source_name, {
"Employee Loan Application": {
"doctype": "Employee Loan",
"validation": {
"docstatus": ["=", 1]
}
}
}, target_doc)
return doclist

View File

@@ -12,7 +12,7 @@ from erpnext.hr.doctype.employee_onboarding.employee_onboarding import Incomplet
class TestEmployeeOnboarding(unittest.TestCase): class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self): def test_employee_onboarding_incomplete_task(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
return frappe.get_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up() _set_up()
applicant = get_job_applicant() applicant = get_job_applicant()
onboarding = frappe.new_doc('Employee Onboarding') onboarding = frappe.new_doc('Employee Onboarding')
@@ -39,9 +39,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
# complete the task # complete the task
project = frappe.get_doc('Project', onboarding.project) project = frappe.get_doc('Project', onboarding.project)
project.load_tasks() for task in frappe.get_all('Task', dict(project=project.name)):
project.tasks[0].status = 'Completed' task = frappe.get_doc('Task', task.name)
project.save() task.status = 'Completed'
task.save()
# make employee # make employee
onboarding.reload() onboarding.reload()
@@ -71,4 +72,3 @@ def _set_up():
project = "Employee Onboarding : Test Researcher - test@researcher.com" project = "Employee Onboarding : Test Researcher - test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project) frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project) frappe.db.sql("delete from tabTask where project=%s", project)
frappe.db.sql("delete from `tabProject Task` where parent=%s", project)

View File

@@ -10,33 +10,36 @@ from erpnext.accounts.doctype.account.test_account import create_account
test_records = frappe.get_test_records('Expense Claim') test_records = frappe.get_test_records('Expense Claim')
test_dependencies = ['Employee'] test_dependencies = ['Employee']
company_name = '_Test Company 4'
class TestExpenseClaim(unittest.TestCase): class TestExpenseClaim(unittest.TestCase):
def test_total_expense_claim_for_project(self): def test_total_expense_claim_for_project(self):
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """) frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
frappe.db.sql("""delete from `tabProject Task` where parent = "_Test Project 1" """)
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
frappe.db.sql("delete from `tabExpense Claim` where project='_Test Project 1'") frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
frappe.get_doc({ frappe.get_doc({
"project_name": "_Test Project 1", "project_name": "_Test Project 1",
"doctype": "Project", "doctype": "Project"
}).save() }).save()
task = frappe.get_doc({ task = frappe.get_doc(dict(
"doctype": "Task", doctype = 'Task',
"subject": "_Test Project Task 1", subject = '_Test Project Task 1',
"project": "_Test Project 1" status = 'Open',
}).save() project = '_Test Project 1'
)).insert()
task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"}) task_name = task.name
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account(company_name)
make_expense_claim(payable_account, 300, 200, "Wind Power LLC","Travel Expenses - WP", "_Test Project 1", task_name)
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
expense_claim2 = make_expense_claim(payable_account, 600, 500, "Wind Power LLC", "Travel Expenses - WP","_Test Project 1", task_name) expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700)
@@ -48,8 +51,8 @@ class TestExpenseClaim(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
def test_expense_claim_status(self): def test_expense_claim_status(self):
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account(company_name)
expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP") expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4")
je_dict = make_bank_entry("Expense Claim", expense_claim.name) je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict) je = frappe.get_doc(je_dict)
@@ -66,9 +69,9 @@ class TestExpenseClaim(unittest.TestCase):
self.assertEqual(expense_claim.status, "Unpaid") self.assertEqual(expense_claim.status, "Unpaid")
def test_expense_claim_gl_entry(self): def test_expense_claim_gl_entry(self):
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account(company_name)
taxes = generate_taxes() taxes = generate_taxes()
expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP", do_not_submit=True, taxes=taxes) expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes)
expense_claim.submit() expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
@@ -78,9 +81,9 @@ class TestExpenseClaim(unittest.TestCase):
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict((d[0], d) for d in [
['CGST - WP',10.0, 0.0], ['CGST - _TC4',18.0, 0.0],
[payable_account, 0.0, 210.0], [payable_account, 0.0, 218.0],
["Travel Expenses - WP", 200.0, 0.0] ["Travel Expenses - _TC4", 200.0, 0.0]
]) ])
for gle in gl_entries: for gle in gl_entries:
@@ -89,14 +92,14 @@ class TestExpenseClaim(unittest.TestCase):
self.assertEquals(expected_values[gle.account][2], gle.credit) self.assertEquals(expected_values[gle.account][2], gle.credit)
def test_rejected_expense_claim(self): def test_rejected_expense_claim(self):
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account(company_name)
expense_claim = frappe.get_doc({ expense_claim = frappe.get_doc({
"doctype": "Expense Claim", "doctype": "Expense Claim",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"payable_account": payable_account, "payable_account": payable_account,
"approval_status": "Rejected", "approval_status": "Rejected",
"expenses": "expenses":
[{ "expense_type": "Travel", "default_account": "Travel Expenses - WP", "amount": 300, "sanctioned_amount": 200 }] [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
}) })
expense_claim.submit() expense_claim.submit()
@@ -111,9 +114,9 @@ def get_payable_account(company):
def generate_taxes(): def generate_taxes():
parent_account = frappe.db.get_value('Account', parent_account = frappe.db.get_value('Account',
{'company': "Wind Power LLC", 'is_group':1, 'account_type': 'Tax'}, {'company': company_name, 'is_group':1, 'account_type': 'Tax'},
'name') 'name')
account = create_account(company="Wind Power LLC", account_name="CGST", account_type="Tax", parent_account=parent_account) account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{ return {'taxes':[{
"account_head": account, "account_head": account,
"rate": 0, "rate": 0,
@@ -124,15 +127,18 @@ def generate_taxes():
def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None):
employee = frappe.db.get_value("Employee", {"status": "Active"}) employee = frappe.db.get_value("Employee", {"status": "Active"})
currency = frappe.db.get_value('Company', company, 'default_currency')
expense_claim = { expense_claim = {
"doctype": "Expense Claim", "doctype": "Expense Claim",
"employee": employee, "employee": employee,
"payable_account": payable_account, "payable_account": payable_account,
"approval_status": "Approved", "approval_status": "Approved",
"company": company, "company": company,
'currency': currency,
"expenses": "expenses":
[{"expense_type": "Travel", [{"expense_type": "Travel",
"default_account": account, "default_account": account,
'currency': currency,
"amount": amount, "amount": amount,
"sanctioned_amount": sanctioned_amount}]} "sanctioned_amount": sanctioned_amount}]}
if taxes: if taxes:

View File

@@ -39,31 +39,19 @@ frappe.ui.form.on('Loan', {
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus == 1 && frm.doc.status == "Sanctioned") { if (frm.doc.docstatus == 1) {
frm.add_custom_button(__('Create Disbursement Entry'), function() { if (frm.doc.status == "Sanctioned") {
frm.trigger("make_jv"); frm.add_custom_button(__('Create Disbursement Entry'), function() {
}) frm.trigger("make_jv");
} }).addClass("btn-primary");
if (frm.doc.repayment_schedule) { } else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) {
let total_amount_paid = 0; frm.add_custom_button(__('Create Repayment Entry'), function() {
$.each(frm.doc.repayment_schedule || [], function(i, row) { frm.trigger("make_repayment_entry");
if (row.paid) { }).addClass("btn-primary");
total_amount_paid += row.total_payment; }
}
});
frm.set_value("total_amount_paid", total_amount_paid);
; }
if (frm.doc.docstatus == 1 && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) {
frm.add_custom_button(__('Create Repayment Entry'), function() {
frm.trigger("make_repayment_entry");
})
} }
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
}, },
status: function (frm) {
frm.toggle_reqd("disbursement_date", frm.doc.status == 'Disbursed')
frm.toggle_reqd("repayment_start_date", frm.doc.status == 'Disbursed')
},
make_jv: function (frm) { make_jv: function (frm) {
frappe.call({ frappe.call({

View File

@@ -1,5 +1,6 @@
{ {
"allow_copy": 0, "allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0, "allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 0, "allow_rename": 0,
@@ -20,6 +21,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicant_type", "fieldname": "applicant_type",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
@@ -53,6 +55,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicant", "fieldname": "applicant",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"hidden": 0, "hidden": 0,
@@ -86,6 +89,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicant_name", "fieldname": "applicant_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@@ -118,6 +122,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "loan_application", "fieldname": "loan_application",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -151,6 +156,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "loan_type", "fieldname": "loan_type",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -184,6 +190,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
@@ -215,7 +222,8 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "", "default": "Today",
"fetch_if_empty": 0,
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "hidden": 0,
@@ -248,6 +256,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -282,6 +291,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "Sanctioned", "default": "Sanctioned",
"fetch_if_empty": 0,
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
@@ -299,7 +309,7 @@
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 1,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -316,6 +326,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:doc.applicant_type==\"Employee\"", "depends_on": "eval:doc.applicant_type==\"Employee\"",
"fetch_if_empty": 0,
"fieldname": "repay_from_salary", "fieldname": "repay_from_salary",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@@ -348,6 +359,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@@ -380,6 +392,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "loan_amount", "fieldname": "loan_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "hidden": 0,
@@ -415,6 +428,7 @@
"columns": 0, "columns": 0,
"default": "", "default": "",
"fetch_from": "loan_type.rate_of_interest", "fetch_from": "loan_type.rate_of_interest",
"fetch_if_empty": 0,
"fieldname": "rate_of_interest", "fieldname": "rate_of_interest",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "hidden": 0,
@@ -448,6 +462,8 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:doc.status==\"Disbursed\"",
"fetch_if_empty": 0,
"fieldname": "disbursement_date", "fieldname": "disbursement_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "hidden": 0,
@@ -480,6 +496,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "repayment_start_date", "fieldname": "repayment_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "hidden": 0,
@@ -499,7 +516,7 @@
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
@@ -512,6 +529,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
@@ -544,6 +562,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "Repay Over Number of Periods", "default": "Repay Over Number of Periods",
"fetch_if_empty": 0,
"fieldname": "repayment_method", "fieldname": "repayment_method",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
@@ -579,6 +598,7 @@
"columns": 0, "columns": 0,
"default": "", "default": "",
"depends_on": "", "depends_on": "",
"fetch_if_empty": 0,
"fieldname": "repayment_periods", "fieldname": "repayment_periods",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@@ -613,6 +633,7 @@
"columns": 0, "columns": 0,
"default": "", "default": "",
"depends_on": "", "depends_on": "",
"fetch_if_empty": 0,
"fieldname": "monthly_repayment_amount", "fieldname": "monthly_repayment_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "hidden": 0,
@@ -646,6 +667,7 @@
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "account_info", "fieldname": "account_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@@ -678,6 +700,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "mode_of_payment", "fieldname": "mode_of_payment",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -711,6 +734,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "payment_account", "fieldname": "payment_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -744,6 +768,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
@@ -775,6 +800,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "loan_account", "fieldname": "loan_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -808,6 +834,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "interest_income_account", "fieldname": "interest_income_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -841,6 +868,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_15", "fieldname": "section_break_15",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@@ -873,6 +901,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "repayment_schedule", "fieldname": "repayment_schedule",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0, "hidden": 0,
@@ -906,6 +935,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_17", "fieldname": "section_break_17",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@@ -939,6 +969,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "total_payment", "fieldname": "total_payment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "hidden": 0,
@@ -972,6 +1003,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
@@ -1004,6 +1036,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "0", "default": "0",
"fetch_if_empty": 0,
"fieldname": "total_interest_payable", "fieldname": "total_interest_payable",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "hidden": 0,
@@ -1037,6 +1070,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "total_amount_paid", "fieldname": "total_amount_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "hidden": 0,
@@ -1070,6 +1104,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@@ -1106,7 +1141,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:53.267145", "modified": "2019-07-10 13:04:20.953694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Loan", "name": "Loan",
@@ -1149,7 +1184,6 @@
"set_user_permissions": 0, "set_user_permissions": 0,
"share": 0, "share": 0,
"submit": 0, "submit": 0,
"user_permission_doctypes": "[\"Employee\"]",
"write": 0 "write": 0
} }
], ],

View File

@@ -6,29 +6,33 @@ from __future__ import unicode_literals
import frappe, math, json import frappe, math, json
import erpnext import erpnext
from frappe import _ from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate from frappe.utils import flt, rounded, add_months, nowdate, getdate
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController): class Loan(AccountsController):
def validate(self): def validate(self):
check_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods) validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods)
self.set_missing_fields()
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def set_missing_fields(self):
if not self.company: if not self.company:
self.company = erpnext.get_default_company() self.company = erpnext.get_default_company()
if not self.posting_date: if not self.posting_date:
self.posting_date = nowdate() self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest: if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods": if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.status == "Repaid/Closed": if self.status == "Repaid/Closed":
self.total_amount_paid = self.total_payment self.total_amount_paid = self.total_payment
if self.status == 'Disbursed' and self.repayment_start_date < self.disbursement_date:
frappe.throw(_("Repayment Start Date cannot be before Disbursement Date."))
if self.status == "Disbursed":
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def make_jv_entry(self): def make_jv_entry(self):
self.check_permission('write') self.check_permission('write')
@@ -105,20 +109,31 @@ def update_total_amount_paid(doc):
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def update_disbursement_status(doc): def update_disbursement_status(doc):
disbursement = frappe.db.sql("""select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount disbursement = frappe.db.sql("""
from `tabGL Entry` where account = %s and against_voucher_type = 'Loan' and against_voucher = %s""", select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount
(doc.payment_account, doc.name), as_dict=1)[0] from `tabGL Entry`
if disbursement.disbursed_amount == doc.loan_amount: where account = %s and against_voucher_type = 'Loan' and against_voucher = %s
frappe.db.set_value("Loan", doc.name , "status", "Disbursed") """, (doc.payment_account, doc.name), as_dict=1)[0]
if disbursement.disbursed_amount == 0:
frappe.db.set_value("Loan", doc.name , "status", "Sanctioned")
if disbursement.disbursed_amount > doc.loan_amount:
frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount))
if disbursement.disbursed_amount > 0:
frappe.db.set_value("Loan", doc.name , "disbursement_date", disbursement.posting_date)
frappe.db.set_value("Loan", doc.name , "repayment_start_date", disbursement.posting_date)
def check_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods): disbursement_date = None
if not disbursement or disbursement.disbursed_amount == 0:
status = "Sanctioned"
elif disbursement.disbursed_amount == doc.loan_amount:
disbursement_date = disbursement.posting_date
status = "Disbursed"
elif disbursement.disbursed_amount > doc.loan_amount:
frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount))
if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")):
frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date"))
frappe.db.sql("""
update `tabLoan`
set status = %s, disbursement_date = %s
where name = %s
""", (status, disbursement_date, doc.name))
def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods):
if repayment_method == "Repay Over Number of Periods" and not repayment_periods: if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods")) frappe.throw(_("Please enter Repayment Periods"))
@@ -222,4 +237,4 @@ def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_a
"reference_name": loan, "reference_name": loan,
}) })
journal_entry.set("accounts", account_amt_list) journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict() return journal_entry.as_dict()

View File

@@ -23,9 +23,8 @@ frappe.ui.form.on('Loan Application', {
}, },
add_toolbar_buttons: function(frm) { add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") { if (frm.doc.status == "Approved") {
frm.add_custom_button(__('Loan'), function() { frm.add_custom_button(__('Create Loan'), function() {
frappe.call({ frappe.call({
type: "GET",
method: "erpnext.hr.doctype.loan_application.loan_application.make_loan", method: "erpnext.hr.doctype.loan_application.loan_application.make_loan",
args: { args: {
"source_name": frm.doc.name "source_name": frm.doc.name
@@ -37,7 +36,7 @@ frappe.ui.form.on('Loan Application', {
} }
} }
}); });
}) }).addClass("btn-primary");
} }
} }
}); });

View File

@@ -9,11 +9,11 @@ from frappe.utils import flt, rounded
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, check_repayment_method from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method
class LoanApplication(Document): class LoanApplication(Document):
def validate(self): def validate(self):
check_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods) validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods)
self.validate_loan_amount() self.validate_loan_amount()
self.get_repayment_details() self.get_repayment_details()
@@ -29,14 +29,14 @@ class LoanApplication(Document):
if self.repayment_method == "Repay Fixed Amount per Period": if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate: if monthly_interest_rate:
self.repayment_periods = math.ceil((math.log(self.repayment_amount) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - (self.loan_amount*monthly_interest_rate))) / math.log(self.repayment_amount - (self.loan_amount*monthly_interest_rate))) /
(math.log(1 + monthly_interest_rate))) (math.log(1 + monthly_interest_rate)))
else: else:
self.repayment_periods = self.loan_amount / self.repayment_amount self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount() self.calculate_payable_amount()
def calculate_payable_amount(self): def calculate_payable_amount(self):
balance_amount = self.loan_amount balance_amount = self.loan_amount
self.total_payable_amount = 0 self.total_payable_amount = 0
@@ -47,9 +47,9 @@ class LoanApplication(Document):
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest self.total_payable_amount = self.loan_amount + self.total_payable_interest
@frappe.whitelist() @frappe.whitelist()
def make_loan(source_name, target_doc = None): def make_loan(source_name, target_doc = None):
doclist = get_mapped_doc("Loan Application", source_name, { doclist = get_mapped_doc("Loan Application", source_name, {

View File

@@ -594,6 +594,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty, sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty,
item.description, item.description,
item.image, item.image,
bom.project,
item.stock_uom, item.stock_uom,
item.allow_alternative_item, item.allow_alternative_item,
item_default.default_warehouse, item_default.default_warehouse,

View File

@@ -615,4 +615,7 @@ erpnext.patches.v11_1.set_missing_opportunity_from
erpnext.patches.v12_0.set_quotation_status erpnext.patches.v12_0.set_quotation_status
erpnext.patches.v12_0.set_priority_for_support erpnext.patches.v12_0.set_priority_for_support
erpnext.patches.v12_0.delete_priority_property_setter erpnext.patches.v12_0.delete_priority_property_setter
execute:frappe.delete_doc("DocType", "Project Task")
erpnext.patches.v11_1.update_default_supplier_in_item_defaults
erpnext.patches.v12_0.update_due_date_in_gle
erpnext.patches.v12_0.add_default_buying_selling_terms_in_company erpnext.patches.v12_0.add_default_buying_selling_terms_in_company

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2018, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
'''
default supplier was not set in the item defaults for multi company instance,
this patch will set the default supplier
'''
if not frappe.db.has_column('Item', 'default_supplier'):
return
frappe.reload_doc('stock', 'doctype', 'item_default')
frappe.reload_doc('stock', 'doctype', 'item')
companies = frappe.get_all("Company")
if len(companies) > 1:
frappe.db.sql(""" UPDATE `tabItem Default`, `tabItem`
SET `tabItem Default`.default_supplier = `tabItem`.default_supplier
WHERE
`tabItem Default`.parent = `tabItem`.name and `tabItem Default`.default_supplier is null
and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """)

View File

@@ -33,19 +33,23 @@ def set_priorities_service_level():
service_level_priorities = frappe.get_list("Service Level", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"]) service_level_priorities = frappe.get_list("Service Level", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"])
frappe.reload_doc("support", "doctype", "service_level") frappe.reload_doc("support", "doctype", "service_level")
frappe.reload_doc("support", "doctype", "support_settings")
frappe.db.set_value('Support Settings', None, 'track_service_level_agreement', 1)
for service_level in service_level_priorities: for service_level in service_level_priorities:
if service_level: if service_level:
doc = frappe.get_doc("Service Level", service_level.name) doc = frappe.get_doc("Service Level", service_level.name)
doc.append("priorities", { if not doc.priorities:
"priority": service_level.priority, doc.append("priorities", {
"default_priority": 1, "priority": service_level.priority,
"response_time": service_level.response_time, "default_priority": 1,
"response_time_period": service_level.response_time_period, "response_time": service_level.response_time,
"resolution_time": service_level.resolution_time, "response_time_period": service_level.response_time_period,
"resolution_time_period": service_level.resolution_time_period "resolution_time": service_level.resolution_time,
}) "resolution_time_period": service_level.resolution_time_period
doc.save(ignore_permissions=True) })
doc.flags.ignore_validate = True
doc.save(ignore_permissions=True)
except frappe.db.TableMissingError: except frappe.db.TableMissingError:
frappe.reload_doc("support", "doctype", "service_level") frappe.reload_doc("support", "doctype", "service_level")
@@ -73,6 +77,7 @@ def set_priorities_service_level_agreement():
"resolution_time": service_level_agreement.resolution_time, "resolution_time": service_level_agreement.resolution_time,
"resolution_time_period": service_level_agreement.resolution_time_period "resolution_time_period": service_level_agreement.resolution_time_period
}) })
doc.flags.ignore_validate = True
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
except frappe.db.TableMissingError: except frappe.db.TableMissingError:
frappe.reload_doc("support", "doctype", "service_level_agreement") frappe.reload_doc("support", "doctype", "service_level_agreement")

View File

@@ -2,10 +2,9 @@ import frappe
def execute(): def execute():
frappe.reload_doctype('Task') frappe.reload_doctype('Task')
frappe.reload_doctype('Project Task')
# add "Completed" if customized # add "Completed" if customized
for doctype in ('Task', 'Project Task'): for doctype in ('Task'):
property_setter_name = frappe.db.exists('Property Setter', dict(doc_type = doctype, field_name = 'status', property = 'options')) property_setter_name = frappe.db.exists('Property Setter', dict(doc_type = doctype, field_name = 'status', property = 'options'))
if property_setter_name: if property_setter_name:
property_setter = frappe.get_doc('Property Setter', property_setter_name) property_setter = frappe.get_doc('Property Setter', property_setter_name)

View File

@@ -0,0 +1,17 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("accounts", "doctype", "gl_entry")
for doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]:
frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype))
frappe.db.sql(""" UPDATE `tabGL Entry`, `tab{doctype}`
SET
`tabGL Entry`.due_date = `tab{doctype}`.due_date
WHERE
`tabGL Entry`.voucher_no = `tab{doctype}`.name and `tabGL Entry`.party is not null
and `tabGL Entry`.voucher_type in ('Sales Invoice', 'Purchase Invoice', 'Journal Entry')
and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""" #nosec
.format(doctype=doctype))

View File

@@ -1,23 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Project", { frappe.ui.form.on("Project", {
setup: function (frm) {
frm.set_indicator_formatter('title',
function (doc) {
let indicator = 'orange';
if (doc.status == 'Overdue') {
indicator = 'red';
} else if (doc.status == 'Cancelled') {
indicator = 'dark grey';
} else if (doc.status == 'Completed') {
indicator = 'green';
}
return indicator;
}
);
},
onload: function (frm) { onload: function (frm) {
var so = frappe.meta.get_docfield("Project", "sales_order"); var so = frappe.meta.get_docfield("Project", "sales_order");
so.get_route_options_for_new_doc = function (field) { so.get_route_options_for_new_doc = function (field) {
@@ -99,58 +82,4 @@ frappe.ui.form.on("Project", {
}); });
}, },
tasks_refresh: function (frm) {
var grid = frm.get_field('tasks').grid;
grid.wrapper.find('select[data-fieldname="status"]').each(function () {
if ($(this).val() === 'Open') {
$(this).addClass('input-indicator-open');
} else {
$(this).removeClass('input-indicator-open');
}
});
},
status: function(frm) {
if (frm.doc.status === 'Cancelled') {
frappe.confirm(__('Set tasks in this project as cancelled?'), () => {
frm.doc.tasks = frm.doc.tasks.map(task => {
task.status = 'Cancelled';
return task;
});
frm.refresh_field('tasks');
});
}
}
});
frappe.ui.form.on("Project Task", {
edit_task: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
if(doc.task_id) {
frappe.set_route("Form", "Task", doc.task_id);
} else {
frappe.msgprint(__("Save the document first."));
}
},
edit_timesheet: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
frappe.route_options = {"project": frm.doc.project_name, "task": child.task_id};
frappe.set_route("List", "Timesheet");
},
make_timesheet: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
frappe.model.with_doctype('Timesheet', function() {
var doc = frappe.model.get_new_doc('Timesheet');
var row = frappe.model.add_child(doc, 'time_logs');
row.project = frm.doc.project_name;
row.task = child.task_id;
frappe.set_route('Form', doc.doctype, doc.name);
})
},
status: function(frm, doctype, name) {
frm.trigger('tasks_refresh');
},
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -19,10 +19,6 @@ class Project(Document):
return '{0}: {1}'.format(_(self.status), frappe.safe_decode(self.project_name)) return '{0}: {1}'.format(_(self.status), frappe.safe_decode(self.project_name))
def onload(self): def onload(self):
"""Load project tasks for quick view"""
if not self.get('__unsaved') and not self.get("tasks"):
self.load_tasks()
self.set_onload('activity_summary', frappe.db.sql('''select activity_type, self.set_onload('activity_summary', frappe.db.sql('''select activity_type,
sum(hours) as total_hours sum(hours) as total_hours
from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type
@@ -33,57 +29,19 @@ class Project(Document):
def before_print(self): def before_print(self):
self.onload() self.onload()
def load_tasks(self):
"""Load `tasks` from the database"""
if frappe.flags.in_import:
return
project_task_custom_fields = frappe.get_all("Custom Field", {"dt": "Project Task"}, "fieldname")
self.tasks = []
for task in self.get_tasks():
task_map = {
"title": task.subject,
"status": task.status,
"start_date": task.exp_start_date,
"end_date": task.exp_end_date,
"description": task.description,
"task_id": task.name,
"task_weight": task.task_weight
}
self.map_custom_fields(task, task_map, project_task_custom_fields)
self.append("tasks", task_map)
def get_tasks(self):
if self.name is None:
return {}
else:
filters = {"project": self.name}
if self.get("deleted_task_list"):
filters.update({
'name': ("not in", self.deleted_task_list)
})
return frappe.get_all("Task", "*", filters, order_by="exp_start_date asc, status asc")
def validate(self): def validate(self):
self.validate_weights()
self.sync_tasks()
self.tasks = []
self.load_tasks()
if not self.is_new(): if not self.is_new():
self.copy_from_template() self.copy_from_template()
self.validate_dates()
self.send_welcome_email() self.send_welcome_email()
self.update_percent_complete(from_validate=True) self.update_costing()
self.update_percent_complete()
def copy_from_template(self): def copy_from_template(self):
''' '''
Copy tasks from template Copy tasks from template
''' '''
if self.project_template and not len(self.tasks or []): if self.project_template and not frappe.db.get_all('Task', dict(project = self.name), limit=1):
# has a template, and no loaded tasks, so lets create # has a template, and no loaded tasks, so lets create
if not self.expected_start_date: if not self.expected_start_date:
@@ -108,104 +66,6 @@ class Project(Document):
task_weight = task.task_weight task_weight = task.task_weight
)).insert() )).insert()
# reload tasks after project
self.load_tasks()
def validate_dates(self):
if self.tasks:
for d in self.tasks:
if self.expected_start_date:
if d.start_date and getdate(d.start_date) < getdate(self.expected_start_date):
frappe.throw(_("Start date of task <b>{0}</b> cannot be less than <b>{1}</b> expected start date <b>{2}</b>")
.format(d.title, self.name, self.expected_start_date))
if d.end_date and getdate(d.end_date) < getdate(self.expected_start_date):
frappe.throw(_("End date of task <b>{0}</b> cannot be less than <b>{1}</b> expected start date <b>{2}</b>")
.format(d.title, self.name, self.expected_start_date))
if self.expected_end_date:
if d.start_date and getdate(d.start_date) > getdate(self.expected_end_date):
frappe.throw(_("Start date of task <b>{0}</b> cannot be greater than <b>{1}</b> expected end date <b>{2}</b>")
.format(d.title, self.name, self.expected_end_date))
if d.end_date and getdate(d.end_date) > getdate(self.expected_end_date):
frappe.throw(_("End date of task <b>{0}</b> cannot be greater than <b>{1}</b> expected end date <b>{2}</b>")
.format(d.title, self.name, self.expected_end_date))
if self.expected_start_date and self.expected_end_date:
if getdate(self.expected_end_date) < getdate(self.expected_start_date):
frappe.throw(_("Expected End Date can not be less than Expected Start Date"))
def validate_weights(self):
for task in self.tasks:
if task.task_weight is not None:
if task.task_weight < 0:
frappe.throw(_("Task weight cannot be negative"))
def sync_tasks(self):
"""sync tasks and remove table"""
if not hasattr(self, "deleted_task_list"):
self.set("deleted_task_list", [])
if self.flags.dont_sync_tasks: return
task_names = []
existing_task_data = {}
fields = ["title", "status", "start_date", "end_date", "description", "task_weight", "task_id"]
exclude_fieldtype = ["Button", "Column Break",
"Section Break", "Table", "Read Only", "Attach", "Attach Image", "Color", "Geolocation", "HTML", "Image"]
custom_fields = frappe.get_all("Custom Field", {"dt": "Project Task",
"fieldtype": ("not in", exclude_fieldtype)}, "fieldname")
for d in custom_fields:
fields.append(d.fieldname)
for d in frappe.get_all('Project Task',
fields = fields,
filters = {'parent': self.name}):
existing_task_data.setdefault(d.task_id, d)
for t in self.tasks:
if t.task_id:
task = frappe.get_doc("Task", t.task_id)
else:
task = frappe.new_doc("Task")
task.project = self.name
if not t.task_id or self.is_row_updated(t, existing_task_data, fields):
task.update({
"subject": t.title,
"status": t.status,
"exp_start_date": t.start_date,
"exp_end_date": t.end_date,
"description": t.description,
"task_weight": t.task_weight
})
self.map_custom_fields(t, task, custom_fields)
task.flags.ignore_links = True
task.flags.from_project = True
task.flags.ignore_feed = True
if t.task_id:
task.update({
"modified_by": frappe.session.user,
"modified": now()
})
task.run_method("validate")
task.db_update()
else:
task.save(ignore_permissions = True)
task_names.append(task.name)
else:
task_names.append(task.name)
# delete
for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}):
self.deleted_task_list.append(t.name)
def is_row_updated(self, row, existing_task_data, fields): def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True if self.get("__islocal") or not existing_task_data: return True
@@ -215,48 +75,43 @@ class Project(Document):
if row.get(field) != d.get(field): if row.get(field) != d.get(field):
return True return True
def map_custom_fields(self, source, target, custom_fields):
for field in custom_fields:
target.update({
field.fieldname: source.get(field.fieldname)
})
def update_project(self): def update_project(self):
'''Called externally by Task'''
self.update_percent_complete() self.update_percent_complete()
self.update_costing() self.update_costing()
self.db_update()
def after_insert(self): def after_insert(self):
self.copy_from_template() self.copy_from_template()
if self.sales_order: if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name) frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
def update_percent_complete(self, from_validate=False): def update_percent_complete(self):
if not self.tasks: return total = frappe.db.count('Task', dict(project=self.name))
total = frappe.db.sql("""select count(name) from tabTask where project=%s""", self.name)[0][0]
if not total and self.percent_complete: if not total:
self.percent_complete = 0 self.percent_complete = 0
else:
if (self.percent_complete_method == "Task Completion" and total > 0) or (
not self.percent_complete_method and total > 0):
completed = frappe.db.sql("""select count(name) from tabTask where
project=%s and status in ('Cancelled', 'Completed')""", self.name)[0][0]
self.percent_complete = flt(flt(completed) / total * 100, 2)
if (self.percent_complete_method == "Task Completion" and total > 0) or ( if (self.percent_complete_method == "Task Progress" and total > 0):
not self.percent_complete_method and total > 0): progress = frappe.db.sql("""select sum(progress) from tabTask where
completed = frappe.db.sql("""select count(name) from tabTask where project=%s""", self.name)[0][0]
project=%s and status in ('Cancelled', 'Completed')""", self.name)[0][0] self.percent_complete = flt(flt(progress) / total, 2)
self.percent_complete = flt(flt(completed) / total * 100, 2)
if (self.percent_complete_method == "Task Progress" and total > 0): if (self.percent_complete_method == "Task Weight" and total > 0):
progress = frappe.db.sql("""select sum(progress) from tabTask where weight_sum = frappe.db.sql("""select sum(task_weight) from tabTask where
project=%s""", self.name)[0][0] project=%s""", self.name)[0][0]
self.percent_complete = flt(flt(progress) / total, 2) weighted_progress = frappe.db.sql("""select progress, task_weight from tabTask where
project=%s""", self.name, as_dict=1)
if (self.percent_complete_method == "Task Weight" and total > 0): pct_complete = 0
weight_sum = frappe.db.sql("""select sum(task_weight) from tabTask where for row in weighted_progress:
project=%s""", self.name)[0][0] pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum)
weighted_progress = frappe.db.sql("""select progress, task_weight from tabTask where self.percent_complete = flt(flt(pct_complete), 2)
project=%s""", self.name, as_dict=1)
pct_complete = 0
for row in weighted_progress:
pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum)
self.percent_complete = flt(flt(pct_complete), 2)
# don't update status if it is cancelled # don't update status if it is cancelled
if self.status == 'Cancelled': if self.status == 'Cancelled':
@@ -268,9 +123,6 @@ class Project(Document):
else: else:
self.status = "Open" self.status = "Open"
if not from_validate:
self.db_update()
def update_costing(self): def update_costing(self):
from_time_sheet = frappe.db.sql("""select from_time_sheet = frappe.db.sql("""select
sum(costing_amount) as costing_amount, sum(costing_amount) as costing_amount,
@@ -297,7 +149,6 @@ class Project(Document):
self.update_sales_amount() self.update_sales_amount()
self.update_billed_amount() self.update_billed_amount()
self.calculate_gross_margin() self.calculate_gross_margin()
self.db_update()
def calculate_gross_margin(self): def calculate_gross_margin(self):
expense_amount = (flt(self.total_costing_amount) + flt(self.total_expense_claim) expense_amount = (flt(self.total_costing_amount) + flt(self.total_expense_claim)
@@ -348,57 +199,6 @@ class Project(Document):
content=content.format(*messages)) content=content.format(*messages))
user.welcome_email_sent = 1 user.welcome_email_sent = 1
def on_update(self):
self.delete_task()
self.load_tasks()
self.update_project()
self.update_dependencies_on_duplicated_project()
def delete_task(self):
if not self.get('deleted_task_list'): return
for d in self.get('deleted_task_list'):
# unlink project
frappe.db.set_value('Task', d, 'project', '')
self.deleted_task_list = []
def update_dependencies_on_duplicated_project(self):
if self.flags.dont_sync_tasks: return
if not self.copied_from:
self.copied_from = self.name
if self.name != self.copied_from and self.get('__unsaved'):
# duplicated project
dependency_map = {}
for task in self.tasks:
_task = frappe.db.get_value(
'Task',
{"subject": task.title, "project": self.copied_from},
['name', 'depends_on_tasks'],
as_dict=True
)
if _task is None:
continue
name = _task.name
dependency_map[task.title] = [x['subject'] for x in frappe.get_list(
'Task Depends On', {"parent": name}, ['subject'])]
for key, value in iteritems(dependency_map):
task_name = frappe.db.get_value('Task', {"subject": key, "project": self.name })
task_doc = frappe.get_doc('Task', task_name)
for dt in value:
dt_name = frappe.db.get_value('Task', {"subject": dt, "project": self.name})
task_doc.append('depends_on', {"task": dt_name})
task_doc.db_update()
def get_timeline_data(doctype, name): def get_timeline_data(doctype, name):
'''Return timeline for attendance''' '''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*) return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*)

View File

@@ -19,18 +19,18 @@ class TestProject(unittest.TestCase):
project = get_project('Test Project with Template') project = get_project('Test Project with Template')
project.load_tasks() tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
task1 = project.tasks[0] task1 = tasks[0]
self.assertEqual(task1.title, 'Task 1') self.assertEqual(task1.subject, 'Task 1')
self.assertEqual(task1.description, 'Task 1 description') self.assertEqual(task1.description, 'Task 1 description')
self.assertEqual(getdate(task1.start_date), getdate('2019-01-01')) self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
self.assertEqual(getdate(task1.end_date), getdate('2019-01-04')) self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
self.assertEqual(len(project.tasks), 4) self.assertEqual(len(tasks), 4)
task4 = project.tasks[3] task4 = tasks[3]
self.assertEqual(task4.title, 'Task 4') self.assertEqual(task4.subject, 'Task 4')
self.assertEqual(getdate(task4.end_date), getdate('2019-01-06')) self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
def get_project(name): def get_project(name):
template = get_project_template() template = get_project_template()

View File

@@ -1,12 +1,6 @@
[ [
{ {
"project_name": "_Test Project", "project_name": "_Test Project",
"status": "Open", "status": "Open"
"tasks":[
{
"title": "_Test Task",
"status": "Open"
}
]
} }
] ]

View File

@@ -1,430 +0,0 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-02-22 11:15:28.201059",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "title",
"fieldtype": "Data",
"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": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"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": "Status",
"length": 0,
"no_copy": 1,
"options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "task_id",
"fieldname": "edit_task",
"fieldtype": "Button",
"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": "View Task",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "edit_timesheet",
"fieldtype": "Button",
"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": "View Timesheet",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "make_timesheet",
"fieldtype": "Button",
"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": "Make Timesheet",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"default": "",
"fieldname": "end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "task_weight",
"fieldtype": "Float",
"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": "Weight",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"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": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "task_id",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Task ID",
"length": 0,
"no_copy": 1,
"options": "Task",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-19 12:30:52.648868",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Task",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"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,
"track_views": 0
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ProjectTask(Document):
pass

View File

@@ -158,12 +158,6 @@ class Task(NestedSet):
if check_if_child_exists(self.name): if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You can not delete this Task.")) throw(_("Child Task exists for this Task. You can not delete this Task."))
if self.project:
tasks = frappe.get_doc('Project', self.project).tasks
for task in tasks:
if task.get('task_id') == self.name:
frappe.delete_doc('Project Task', task.name)
self.update_nsm_model() self.update_nsm_model()
def update_status(self): def update_status(self):

View File

@@ -109,7 +109,7 @@ class CallPopup {
}); });
wrapper.append(` wrapper.append(`
<div class="caller-info flex"> <div class="caller-info flex">
${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)} ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image || '')}
<div> <div>
<h5>${contact_name}</h5> <h5>${contact_name}</h5>
<div>${contact.mobile_no || ''}</div> <div>${contact.mobile_no || ''}</div>

View File

@@ -52,3 +52,13 @@ $.extend(frappe.breadcrumbs.preferred, {
"Sales Partner": "Selling", "Sales Partner": "Selling",
"Brand": "Selling" "Brand": "Selling"
}); });
$.extend(frappe.breadcrumbs.module_map, {
'ERPNext Integrations': 'Integrations',
'Geo': 'Settings',
'Accounts': 'Accounting',
'Portal': 'Website',
'Utilities': 'Settings',
'Shopping Cart': 'Website',
'Contacts': 'CRM'
});

View File

@@ -141,6 +141,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
price_list_rate: function(doc, cdt, cdn) { price_list_rate: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]); frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
let item_rate = item.price_list_rate; let item_rate = item.price_list_rate;
@@ -154,6 +155,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
if (item.discount_amount) { if (item.discount_amount) {
item.rate = flt((item.price_list_rate) - (item.discount_amount), precision('rate', item)); item.rate = flt((item.price_list_rate) - (item.discount_amount), precision('rate', item));
} else {
item.rate = item_rate;
} }
this.calculate_taxes_and_totals(); this.calculate_taxes_and_totals();

View File

@@ -129,9 +129,7 @@ function get_filters(){
} }
] ]
let dimension_filters = erpnext.get_dimension_filters(); erpnext.dimension_filters.then((dimensions) => {
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => { dimensions.forEach((dimension) => {
filters.push({ filters.push({
"fieldname": dimension["fieldname"], "fieldname": dimension["fieldname"],

View File

@@ -20,8 +20,10 @@ erpnext.SMSManager = function SMSManager(doc) {
'Purchase Receipt' : 'Items has been received against purchase receipt: ' + doc.name 'Purchase Receipt' : 'Items has been received against purchase receipt: ' + doc.name
} }
if (in_list(['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'], doc.doctype)) if (in_list(['Sales Order', 'Delivery Note', 'Sales Invoice'], doc.doctype))
this.show(doc.contact_person, 'Customer', doc.customer, '', default_msg[doc.doctype]); this.show(doc.contact_person, 'Customer', doc.customer, '', default_msg[doc.doctype]);
else if (doc.doctype === 'Quotation')
this.show(doc.contact_person, 'Customer', doc.party_name, '', default_msg[doc.doctype]);
else if (in_list(['Purchase Order', 'Purchase Receipt'], doc.doctype)) else if (in_list(['Purchase Order', 'Purchase Receipt'], doc.doctype))
this.show(doc.contact_person, 'Supplier', doc.supplier, '', default_msg[doc.doctype]); this.show(doc.contact_person, 'Supplier', doc.supplier, '', default_msg[doc.doctype]);
else if (doc.doctype == 'Lead') else if (doc.doctype == 'Lead')

View File

@@ -573,7 +573,6 @@ erpnext.utils.map_current_doc = function(opts) {
if(!r.exc) { if(!r.exc) {
var doc = frappe.model.sync(r.message); var doc = frappe.model.sync(r.message);
cur_frm.dirty(); cur_frm.dirty();
erpnext.utils.clear_duplicates();
cur_frm.refresh(); cur_frm.refresh();
} }
} }
@@ -604,28 +603,6 @@ erpnext.utils.map_current_doc = function(opts) {
} }
} }
erpnext.utils.clear_duplicates = function() {
if(!cur_frm.doc.items) return;
const unique_items = new Map();
/*
Create a Map of items with
item_code => [qty, warehouse, batch_no]
*/
let items = [];
for (let item of cur_frm.doc.items) {
if (!(unique_items.has(item.item_code) && unique_items.get(item.item_code)[0] === item.qty &&
unique_items.get(item.item_code)[1] === item.warehouse && unique_items.get(item.item_code)[2] === item.batch_no &&
unique_items.get(item.item_code)[3] === item.delivery_date && unique_items.get(item.item_code)[4] === item.required_date &&
unique_items.get(item.item_code)[5] === item.rate)) {
unique_items.set(item.item_code, [item.qty, item.warehouse, item.batch_no, item.delivery_date, item.required_date, item.rate]);
items.push(item);
}
}
cur_frm.doc.items = items;
}
frappe.form.link_formatters['Item'] = function(value, doc) { frappe.form.link_formatters['Item'] = function(value, doc) {
if(doc && doc.item_name && doc.item_name !== value) { if(doc && doc.item_name && doc.item_name !== value) {
return value? value + ': ' + doc.item_name: doc.item_name; return value? value + ': ' + doc.item_name: doc.item_name;

View File

@@ -7,12 +7,12 @@ erpnext.doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoi
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile"]; "Travel Request", "Fees", "POS Profile"];
let dimension_filters = erpnext.get_dimension_filters(); erpnext.dimension_filters = erpnext.get_dimension_filters();
erpnext.doctypes_with_dimensions.forEach((doctype) => { erpnext.doctypes_with_dimensions.forEach((doctype) => {
frappe.ui.form.on(doctype, { frappe.ui.form.on(doctype, {
onload: function(frm) { onload: function(frm) {
dimension_filters.then((dimensions) => { erpnext.dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => { dimensions.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => { frappe.model.with_doctype(dimension['document_type'], () => {
if (frappe.meta.has_field(dimension['document_type'], 'is_group')) { if (frappe.meta.has_field(dimension['document_type'], 'is_group')) {

View File

@@ -74,7 +74,6 @@ class Gstr1Report(object):
for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv) invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items(): for rate, items in items_based_on_rate.items():
place_of_supply = invoice_details.get("place_of_supply") place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin") ecommerce_gstin = invoice_details.get("ecommerce_gstin")
@@ -85,7 +84,7 @@ class Gstr1Report(object):
"rate": "", "rate": "",
"taxable_value": 0, "taxable_value": 0,
"cess_amount": 0, "cess_amount": 0,
"type": 0 "type": ""
}) })
row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
@@ -94,6 +93,7 @@ class Gstr1Report(object):
row["rate"] = rate row["rate"] = rate
row["taxable_value"] += sum([abs(net_amount) row["taxable_value"] += sum([abs(net_amount)
for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items])
row["cess_amount"] += flt(self.invoice_cess.get(inv), 2)
row["type"] = "E" if ecommerce_gstin else "OE" row["type"] = "E" if ecommerce_gstin else "OE"
for key, value in iteritems(b2cs_output): for key, value in iteritems(b2cs_output):
@@ -123,6 +123,10 @@ class Gstr1Report(object):
row += [tax_rate or 0, taxable_value] row += [tax_rate or 0, taxable_value]
for column in self.other_columns:
if column.get('fieldname') == 'cess_amount':
row.append(flt(self.invoice_cess.get(invoice), 2))
return row, taxable_value return row, taxable_value
def get_invoice_data(self): def get_invoice_data(self):
@@ -327,7 +331,7 @@ class Gstr1Report(object):
"fieldtype": "Data" "fieldtype": "Data"
}, },
{ {
"fieldname": "invoice_type", "fieldname": "gst_category",
"label": "Invoice Type", "label": "Invoice Type",
"fieldtype": "Data" "fieldtype": "Data"
}, },
@@ -564,12 +568,18 @@ def get_json():
out = get_b2b_json(res, gstin) out = get_b2b_json(res, gstin)
gst_json["b2b"] = out gst_json["b2b"] = out
elif filters["type_of_business"] == "B2C Large": elif filters["type_of_business"] == "B2C Large":
for item in report_data[:-1]: for item in report_data[:-1]:
res.setdefault(item["place_of_supply"], []).append(item) res.setdefault(item["place_of_supply"], []).append(item)
out = get_b2cl_json(res, gstin) out = get_b2cl_json(res, gstin)
gst_json["b2cl"] = out gst_json["b2cl"] = out
elif filters["type_of_business"] == "B2C Small":
out = get_b2cs_json(report_data[:-1], gstin)
gst_json["b2cs"] = out
elif filters["type_of_business"] == "EXPORT": elif filters["type_of_business"] == "EXPORT":
for item in report_data[:-1]: for item in report_data[:-1]:
res.setdefault(item["export_type"], []).append(item) res.setdefault(item["export_type"], []).append(item)
@@ -605,6 +615,45 @@ def get_b2b_json(res, gstin):
return out return out
def get_b2cs_json(data, gstin):
company_state_number = gstin[0:2]
out = []
for d in data:
pos = d.get('place_of_supply').split('-')[0]
tax_details = {}
rate = d.get('rate', 0)
tax = flt((d["taxable_value"]*rate)/100.0, 2)
if company_state_number == pos:
tax_details.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)})
else:
tax_details.update({"iamt": tax})
inv = {
"sply_ty": "INTRA" if company_state_number == pos else "INTER",
"pos": pos,
"typ": d.get('type'),
"txval": flt(d.get('taxable_value'), 2),
"rt": rate,
"iamt": flt(tax_details.get('iamt'), 2),
"camt": flt(tax_details.get('camt'), 2),
"samt": flt(tax_details.get('samt'), 2),
"csamt": flt(d.get('cess_amount'), 2)
}
if d.get('type') == "E" and d.get('ecommerce_gstin'):
inv.update({
"etin": d.get('ecommerce_gstin')
})
out.append(inv)
return out
def get_b2cl_json(res, gstin): def get_b2cl_json(res, gstin):
out = [] out = []
for pos in res: for pos in res:

View File

@@ -25,10 +25,6 @@ def get_data():
'label': _('Orders'), 'label': _('Orders'),
'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice']
}, },
{
'label': _('Service Level Agreement'),
'items': ['Service Level Agreement']
},
{ {
'label': _('Payments'), 'label': _('Payments'),
'items': ['Payment Entry'] 'items': ['Payment Entry']

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('Installation Note', { frappe.ui.form.on('Installation Note', {
setup: function(frm) { setup: function(frm) {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} frappe.dynamic_link = {doc: frm.doc, fieldname: 'customer', doctype: 'Customer'}
frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer', erpnext.queries.customer); frm.set_query('customer', erpnext.queries.customer);

View File

@@ -107,7 +107,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
refresh: function(doc, dt, dn) { refresh: function(doc, dt, dn) {
var me = this; var me = this;
this._super(); this._super();
var allow_delivery = false; let allow_delivery = false;
if(doc.docstatus==1) { if(doc.docstatus==1) {
if(this.frm.has_perm("submit")) { if(this.frm.has_perm("submit")) {
@@ -132,6 +132,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
if(doc.status !== 'Closed') { if(doc.status !== 'Closed') {
if(doc.status !== 'On Hold') { if(doc.status !== 'On Hold') {
allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty))
if (this.frm.has_perm("submit")) { if (this.frm.has_perm("submit")) {
if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) { if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) {
// hold // hold

View File

@@ -492,13 +492,27 @@ def close_or_unclose_sales_orders(names, status):
frappe.local.message_log = [] frappe.local.message_log = []
def get_requested_item_qty(sales_order):
return frappe._dict(frappe.db.sql("""
select sales_order_item, sum(stock_qty)
from `tabMaterial Request Item`
where docstatus = 1
and sales_order = %s
group by sales_order_item
""", sales_order))
@frappe.whitelist() @frappe.whitelist()
def make_material_request(source_name, target_doc=None): def make_material_request(source_name, target_doc=None):
requested_item_qty = get_requested_item_qty(source_name)
def postprocess(source, doc): def postprocess(source, doc):
doc.material_request_type = "Purchase" doc.material_request_type = "Purchase"
def update_item(source, target, source_parent): def update_item(source, target, source_parent):
target.project = source_parent.project target.project = source_parent.project
target.qty = source.stock_qty - requested_item_qty.get(source.name, 0)
target.conversion_factor = 1
target.stock_qty = source.stock_qty - requested_item_qty.get(source.name, 0)
doc = get_mapped_doc("Sales Order", source_name, { doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": { "Sales Order": {
@@ -523,7 +537,7 @@ def make_material_request(source_name, target_doc=None):
"stock_uom": "uom", "stock_uom": "uom",
"stock_qty": "qty" "stock_qty": "qty"
}, },
"condition": lambda doc: not frappe.db.exists('Product Bundle', doc.item_code), "condition": lambda doc: not frappe.db.exists('Product Bundle', doc.item_code) and doc.stock_qty > requested_item_qty.get(doc.name, 0),
"postprocess": update_item "postprocess": update_item
} }
}, target_doc, postprocess) }, target_doc, postprocess)
@@ -547,12 +561,6 @@ def make_project(source_name, target_doc=None):
"base_grand_total" : "estimated_costing", "base_grand_total" : "estimated_costing",
} }
}, },
"Sales Order Item": {
"doctype": "Project Task",
"field_map": {
"item_code": "title",
},
}
}, target_doc, postprocess) }, target_doc, postprocess)
return doc return doc

View File

@@ -77,8 +77,34 @@ frappe.ui.form.on("Delivery Note", {
}, },
print_without_amount: function(frm) { print_without_amount: function(frm) {
erpnext.stock.delivery_note.set_print_hide(frm.doc); erpnext.stock.delivery_note.set_print_hide(frm.doc);
},
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.is_return === 1 && frm.doc.per_billed !== 100) {
frm.add_custom_button(__('Credit Note'), function() {
frappe.confirm(__("Are you sure you want to make credit note?"),
function() {
frm.trigger("make_credit_note");
}
);
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
},
make_credit_note: function(frm) {
frm.call({
method: "make_return_invoice",
doc: frm.doc,
freeze: true,
callback: function() {
frm.reload_doc();
}
});
} }
}); });

View File

@@ -333,7 +333,10 @@ class DeliveryNote(SellingController):
return_invoice.is_return = True return_invoice.is_return = True
return_invoice.save() return_invoice.save()
return_invoice.submit() return_invoice.submit()
frappe.msgprint(_("Credit Note {0} has been created automatically").format(return_invoice.name))
credit_note_link = frappe.utils.get_link_to_form('Sales Invoice', return_invoice.name)
frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link))
except: except:
frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"))

View File

@@ -31,13 +31,16 @@ class ItemPrice(Document):
frappe.throw(_("Valid From Date must be lesser than Valid Upto Date.")) frappe.throw(_("Valid From Date must be lesser than Valid Upto Date."))
def update_price_list_details(self): def update_price_list_details(self):
self.buying, self.selling, self.currency = \ if self.price_list:
frappe.db.get_value("Price List", self.buying, self.selling, self.currency = \
{"name": self.price_list, "enabled": 1}, frappe.db.get_value("Price List",
["buying", "selling", "currency"]) {"name": self.price_list, "enabled": 1},
["buying", "selling", "currency"])
def update_item_details(self): def update_item_details(self):
self.item_name, self.item_description = frappe.db.get_value("Item",self.item_code,["item_name", "description"]) if self.item_code:
self.item_name, self.item_description = frappe.db.get_value("Item",
self.item_code,["item_name", "description"])
def check_duplicates(self): def check_duplicates(self):
conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"

View File

@@ -211,6 +211,7 @@ frappe.ui.form.on('Material Request', {
d.stock_uom = item.stock_uom; d.stock_uom = item.stock_uom;
d.conversion_factor = 1; d.conversion_factor = 1;
d.qty = item.qty; d.qty = item.qty;
d.project = item.project;
}); });
} }
d.hide(); d.hide();

View File

@@ -38,6 +38,29 @@ frappe.ui.form.on("Purchase Receipt", {
if(frm.doc.company) { if(frm.doc.company) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
} }
if (frm.doc.docstatus === 1 && frm.doc.is_return === 1 && frm.doc.per_billed !== 100) {
frm.add_custom_button(__('Debit Note'), function() {
frappe.confirm(__("Are you sure you want to make debit note?"),
function() {
frm.trigger("make_debit_note");
}
);
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
},
make_debit_note: function(frm) {
frm.call({
method: "make_return_invoice",
doc: frm.doc,
freeze: true,
callback: function() {
frm.reload_doc();
}
});
}, },
company: function(frm) { company: function(frm) {

View File

@@ -387,6 +387,16 @@ class PurchaseReceipt(BuyingController):
self.load_from_db() self.load_from_db()
def make_return_invoice(self):
return_invoice = make_purchase_invoice(self.name)
return_invoice.is_return = True
return_invoice.save()
return_invoice.submit()
debit_note_link = frappe.utils.get_link_to_form('Purchase Invoice', return_invoice.name)
frappe.msgprint(_("Debit Note {0} has been created automatically").format(debit_note_link))
def update_billed_amount_based_on_po(po_detail, update_modified=True): def update_billed_amount_based_on_po(po_detail, update_modified=True):
# Billed against Sales Order directly # Billed against Sales Order directly
billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`

View File

@@ -129,8 +129,15 @@ frappe.ui.form.on("Issue", {
function set_time_to_resolve_and_response(frm) { function set_time_to_resolve_and_response(frm) {
frm.dashboard.clear_headline(); frm.dashboard.clear_headline();
var time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled); var time_to_respond = get_status(frm.doc.response_by_variance);
var time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled); if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
}
var time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
}
frm.dashboard.set_headline_alert( frm.dashboard.set_headline_alert(
'<div class="row">' + '<div class="row">' +
@@ -146,7 +153,15 @@ function set_time_to_resolve_and_response(frm) {
function get_time_left(timestamp, agreement_fulfilled) { function get_time_left(timestamp, agreement_fulfilled) {
const diff = moment(timestamp).diff(moment()); const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : moment(0, 'seconds').format('HH:mm'); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
let indicator = (diff_display == '00:00' && agreement_fulfilled != "Fulfilled") ? "red" : "green"; let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator}; return {"diff_display": diff_display, "indicator": indicator};
} }
function get_status(variance) {
if (variance > 0) {
return {"diff_display": "Fulfilled", "indicator": "green"};
} else {
return {"diff_display": "Failed", "indicator": "red"};
}
}

View File

@@ -113,6 +113,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"default": "Medium",
"fieldname": "priority", "fieldname": "priority",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
@@ -143,7 +144,6 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "service_level_section", "fieldname": "service_level_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Service Level" "label": "Service Level"
@@ -314,6 +314,7 @@
}, },
{ {
"default": "Ongoing", "default": "Ongoing",
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "agreement_fulfilled", "fieldname": "agreement_fulfilled",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Service Level Agreement Fulfilled", "label": "Service Level Agreement Fulfilled",
@@ -321,6 +322,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.service_level_agreement",
"description": "in hours", "description": "in hours",
"fieldname": "response_by_variance", "fieldname": "response_by_variance",
"fieldtype": "Float", "fieldtype": "Float",
@@ -328,6 +330,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.service_level_agreement",
"description": "in hours", "description": "in hours",
"fieldname": "resolution_by_variance", "fieldname": "resolution_by_variance",
"fieldtype": "Float", "fieldtype": "Float",
@@ -337,7 +340,7 @@
], ],
"icon": "fa fa-ticket", "icon": "fa fa-ticket",
"idx": 7, "idx": 7,
"modified": "2019-06-27 15:19:00.771333", "modified": "2019-06-30 13:19:38.215525",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Issue", "name": "Issue",

View File

@@ -92,7 +92,6 @@ class Issue(Document):
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2) self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2)
self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
self.save(ignore_permissions=True)
def create_communication(self): def create_communication(self):
communication = frappe.new_doc("Communication") communication = frappe.new_doc("Communication")
@@ -118,6 +117,17 @@ class Issue(Document):
replicated_issue = deepcopy(self) replicated_issue = deepcopy(self)
replicated_issue.subject = subject replicated_issue.subject = subject
replicated_issue.creation = now_datetime()
# Reset SLA
if replicated_issue.service_level_agreement:
replicated_issue.service_level_agreement = None
replicated_issue.agreement_fulfilled = "Ongoing"
replicated_issue.response_by = None
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
replicated_issue.resolution_by_variance = None
frappe.get_doc(replicated_issue).insert() frappe.get_doc(replicated_issue).insert()
# Replicate linked Communications # Replicate linked Communications
@@ -136,7 +146,8 @@ class Issue(Document):
return replicated_issue.name return replicated_issue.name
def before_insert(self): def before_insert(self):
self.set_response_and_resolution_time() if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
self.set_response_and_resolution_time()
def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): def set_response_and_resolution_time(self, priority=None, service_level_agreement=None):
service_level_agreement = get_active_service_level_agreement_for(priority=priority, service_level_agreement = get_active_service_level_agreement_for(priority=priority,
@@ -171,13 +182,16 @@ class Issue(Document):
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()))
def change_service_level_agreement_and_priority(self): def change_service_level_agreement_and_priority(self):
if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
frappe.msgprint("Priority has been updated.")
if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): if not self.priority == frappe.db.get_value("Issue", self.name, "priority"):
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
frappe.msgprint("Service Level Agreement has been updated.") frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority))
if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"):
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
def get_expected_time_for(parameter, service_level, start_date_time): def get_expected_time_for(parameter, service_level, start_date_time):
current_date_time = start_date_time current_date_time = start_date_time
@@ -258,15 +272,15 @@ def set_service_level_agreement_variance(issue=None):
if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer
variance = round(time_diff_in_hours(doc.response_by, current_time), 2) variance = round(time_diff_in_hours(doc.response_by, current_time), 2)
frappe.db.set_value("Issue", doc.name, "response_by_variance", variance) frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
if variance < 0: if variance < 0:
frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Failed") frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
if not doc.resolution_date: # resolution_date set when issue has been closed if not doc.resolution_date: # resolution_date set when issue has been closed
variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2)
frappe.db.set_value("Issue", doc.name, "resolution_by_variance", variance) frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
if variance < 0: if variance < 0:
frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Failed") frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
def get_list_context(context=None): def get_list_context(context=None):
return { return {

View File

@@ -11,6 +11,7 @@ from datetime import timedelta
class TestIssue(unittest.TestCase): class TestIssue(unittest.TestCase):
def test_response_time_and_resolution_time_based_on_different_sla(self): def test_response_time_and_resolution_time_based_on_different_sla(self):
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_agreements_for_issues() create_service_level_agreements_for_issues()
creation = datetime.datetime(2019, 3, 4, 12, 0) creation = datetime.datetime(2019, 3, 4, 12, 0)

View File

@@ -5,6 +5,7 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"enable",
"service_level", "service_level",
"default_service_level_agreement", "default_service_level_agreement",
"holiday_list", "holiday_list",
@@ -149,9 +150,15 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Entity Type", "label": "Entity Type",
"options": "\nCustomer\nCustomer Group\nTerritory" "options": "\nCustomer\nCustomer Group\nTerritory"
},
{
"default": "1",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
} }
], ],
"modified": "2019-06-20 18:04:14.293378", "modified": "2019-07-09 17:22:16.402939",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Service Level Agreement", "name": "Service Level Agreement",

View File

@@ -6,19 +6,23 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import getdate
class ServiceLevelAgreement(Document): class ServiceLevelAgreement(Document):
def validate(self): def validate(self):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
frappe.throw(_("Service Level Agreement tracking is not enabled."))
if self.default_service_level_agreement: if self.default_service_level_agreement:
if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}): if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}):
frappe.throw(_("A Default Service Level Agreement already exists.")) frappe.throw(_("A Default Service Level Agreement already exists."))
else: else:
if self.start_date and self.end_date: if self.start_date and self.end_date:
if self.start_date >= self.end_date: if getdate(self.start_date) >= getdate(self.end_date):
frappe.throw(_("Start Date of Agreement can't be greater than or equal to End Date.")) frappe.throw(_("Start Date of Agreement can't be greater than or equal to End Date."))
if self.end_date < frappe.utils.getdate(): if getdate(self.end_date) < getdate(frappe.utils.getdate()):
frappe.throw(_("End Date of Agreement can't be less than today.")) frappe.throw(_("End Date of Agreement can't be less than today."))
if self.entity_type and self.entity: if self.entity_type and self.entity:
@@ -44,12 +48,16 @@ def check_agreement_status():
for service_level_agreement in service_level_agreements: for service_level_agreement in service_level_agreements:
doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name) doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
if doc.end_date and doc.end_date < frappe.utils.getdate(): if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()):
frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0)
def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None): def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
return
filters = [ filters = [
["Service Level Agreement", "active", "=", 1], ["Service Level Agreement", "active", "=", 1],
["Service Level Agreement", "enable", "=", 1]
] ]
if priority: if priority:
@@ -80,6 +88,14 @@ def get_customer_territory(customer):
@frappe.whitelist() @frappe.whitelist()
def get_service_level_agreement_filters(name, customer=None): def get_service_level_agreement_filters(name, customer=None):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
return
filters = [
["Service Level Agreement", "active", "=", 1],
["Service Level Agreement", "enable", "=", 1]
]
if not customer: if not customer:
or_filters = [ or_filters = [
["Service Level Agreement", "default_service_level_agreement", "=", 1] ["Service Level Agreement", "default_service_level_agreement", "=", 1]
@@ -93,5 +109,5 @@ def get_service_level_agreement_filters(name, customer=None):
return { return {
"priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])],
"service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", or_filters=or_filters)] "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)]
} }

View File

@@ -10,6 +10,8 @@ from erpnext.support.doctype.service_level.test_service_level import create_serv
class TestServiceLevelAgreement(unittest.TestCase): class TestServiceLevelAgreement(unittest.TestCase):
def test_service_level_agreement(self): def test_service_level_agreement(self):
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_for_sla() create_service_level_for_sla()
# Default Service Level Agreement # Default Service Level Agreement

View File

@@ -1,560 +1,145 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-02-17 13:07:35.686409", "creation": "2017-02-17 13:07:35.686409",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"sb_00",
"track_service_level_agreement",
"issues_sb",
"close_issue_after_days",
"portal_sb",
"get_started_sections",
"show_latest_forum_posts",
"forum_sb",
"forum_url",
"get_latest_query",
"response_key_list",
"column_break_10",
"post_title_key",
"post_description_key",
"post_route_key",
"post_route_string",
"search_apis_sb",
"search_apis"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "issues_sb", "fieldname": "issues_sb",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Issues"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Issues",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "7", "default": "7",
"description": "Auto close Issue after 7 days", "description": "Auto close Issue after 7 days",
"fieldname": "close_issue_after_days", "fieldname": "close_issue_after_days",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "label": "Close Issue After Days"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Close Issue After Days",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "portal_sb", "fieldname": "portal_sb",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Support Portal"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Support Portal",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_started_sections", "fieldname": "get_started_sections",
"fieldtype": "Code", "fieldtype": "Code",
"hidden": 0, "label": "Get Started Sections"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Get Started Sections",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "show_latest_forum_posts", "fieldname": "show_latest_forum_posts",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Show Latest Forum Posts"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Show Latest Forum Posts",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "show_latest_forum_posts", "depends_on": "show_latest_forum_posts",
"fieldname": "forum_sb", "fieldname": "forum_sb",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Forum Posts"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Forum Posts",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "forum_url", "fieldname": "forum_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Forum URL"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Forum URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_latest_query", "fieldname": "get_latest_query",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Get Latest Query"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Get Latest Query",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "response_key_list", "fieldname": "response_key_list",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Response Key List"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response Key List",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "post_title_key", "fieldname": "post_title_key",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Post Title Key"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Post Title Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "post_description_key", "fieldname": "post_description_key",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Post Description Key"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Post Description Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "post_route_key", "fieldname": "post_route_key",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Post Route Key"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Post Route Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "post_route_string", "fieldname": "post_route_string",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Post Route String"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Post Route String",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "search_apis_sb", "fieldname": "search_apis_sb",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Search APIs"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Search APIs",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "search_apis", "fieldname": "search_apis",
"fieldtype": "Table", "fieldtype": "Table",
"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": "Search APIs", "label": "Search APIs",
"length": 0, "options": "Support Search Source"
"no_copy": 0, },
"options": "Support Search Source", {
"permlevel": 0, "fieldname": "sb_00",
"precision": "", "fieldtype": "Section Break",
"print_hide": 0, "label": "Service Level Agreements"
"print_hide_if_no_value": 0, },
"read_only": 0, {
"remember_last_selected_value": 0, "default": "0",
"report_hide": 0, "fieldname": "track_service_level_agreement",
"reqd": 0, "fieldtype": "Check",
"search_index": 0, "label": "Track Service Level Agreement"
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "modified": "2019-07-09 17:11:38.216732",
"max_attachments": 0,
"modified": "2018-05-17 02:11:33.462444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Support Settings", "name": "Support Settings",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }