Merge branch 'develop' of https://github.com/frappe/erpnext into accounting_dimension_filters

This commit is contained in:
Deepesh Garg 2021-01-15 15:43:59 +05:30
commit cbed6f970a
115 changed files with 2871 additions and 1531 deletions

View File

@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -159,10 +164,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@ -181,12 +186,14 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
project=project)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(frappe.db.get_value("GL Entry",
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@ -289,7 +296,7 @@ def make_budget(**args):
budget = frappe.new_doc("Budget") budget = frappe.new_doc("Budget")
if budget_against == "Project": if budget_against == "Project":
budget.project = "_Test Project" budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget.cost_center =cost_center or "_Test Cost Center - _TC" budget.cost_center =cost_center or "_Test Cost Center - _TC"

View File

@ -157,9 +157,10 @@ class GLEntry(Document):
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group(): if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""") and self.cost_center and _check_is_group():
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)

View File

@ -20,7 +20,8 @@ def get_data():
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
}, },
{ {
'items': ['Item'] 'label': _('Stock'),
'items': ['Item Groups', 'Item']
} }
] ]
} }

View File

@ -160,7 +160,7 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_reverse_journal_entry(self): def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False) "Sales - _TC", 100, exchange_rate=50, save=False)
@ -299,15 +299,20 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_project(self): def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
project = make_project({
'project_name': 'Journal Entry Project', if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
'project_template_name': 'Test Project Template', project = make_project({
'start_date': '2020-01-01' 'project_name': 'Journal Entry Project',
}) 'project_template_name': 'Test Project Template',
'start_date': '2020-01-01'
})
project_name = project.name
else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
for d in jv.accounts: for d in jv.accounts:
d.project = project.project_name d.project = project_name
jv.voucher_type = "Bank Entry" jv.voucher_type = "Bank Entry"
jv.multi_currency = 0 jv.multi_currency = 0
jv.cheque_no = "112233" jv.cheque_no = "112233"
@ -317,10 +322,10 @@ class TestJournalEntry(unittest.TestCase):
expected_values = { expected_values = {
"_Test Cash - _TC": { "_Test Cash - _TC": {
"project": project.project_name "project": project_name
}, },
"_Test Bank - _TC": { "_Test Bank - _TC": {
"project": project.project_name "project": project_name
} }
} }

View File

@ -396,6 +396,8 @@ frappe.ui.form.on('Payment Entry', {
set_account_currency_and_balance: function(frm, account, currency_field, set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) { balance_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) { if (frm.doc.posting_date && account) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@ -422,6 +424,14 @@ frappe.ui.form.on('Payment Entry', {
if(!frm.doc.paid_amount && frm.doc.received_amount) if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm); frm.events.received_amount(frm);
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
&& frm.doc.paid_amount != frm.doc.received_amount) {
if (company_currency != frm.doc.paid_from_account_currency &&
frm.doc.payment_type == "Pay") {
frm.doc.paid_amount = frm.doc.received_amount;
}
}
} }
}, },
() => { () => {

View File

@ -505,7 +505,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
frappe.ui.form.on("Purchase Invoice", { frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Purchase Invoice': 'Debit Note', 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
} }

View File

@ -426,26 +426,31 @@ class TestPurchaseInvoice(unittest.TestCase):
) )
def test_total_purchase_cost_for_project(self): def test_total_purchase_cost_for_project(self):
make_project({'project_name':'_Test Project'}) if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
project = make_project({'project_name':'_Test Project for Purchase'})
else:
project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") from `tabPurchase Invoice Item`
where project = '{0}'
and docstatus=1""".format(project.name))
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project") pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi1 = make_purchase_invoice(qty=10, project="_Test Project") pi1 = make_purchase_invoice(qty=10, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500) existing_purchase_cost + 15500)
pi1.cancel() pi1.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi.cancel() pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice_with_perpetual_inventory(self): def test_return_purchase_invoice_with_perpetual_inventory(self):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
@ -860,17 +865,17 @@ class TestPurchaseInvoice(unittest.TestCase):
}) })
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
pi.items[0].project = item_project.project_name pi.items[0].project = item_project.name
pi.project = project.project_name pi.project = project.name
pi.submit() pi.submit()
expected_values = { expected_values = {
"Creditors - _TC": { "Creditors - _TC": {
"project": project.project_name "project": project.name
}, },
"_Test Account Cost for Goods Sold - _TC": { "_Test Account Cost for Goods Sold - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }

View File

@ -587,7 +587,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery', 'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return', 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request', 'Payment Request': 'Payment Request',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
}, },

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@ -179,7 +179,7 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
@ -261,10 +261,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@ -549,7 +549,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended
@ -1694,6 +1699,7 @@ def get_mode_of_payment_info(mode_of_payment, company):
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1) (company, mode_of_payment), as_dict=1)
@frappe.whitelist()
def create_dunning(source_name, target_doc=None): def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount

View File

@ -1573,17 +1573,17 @@ class TestSalesInvoice(unittest.TestCase):
}) })
sales_invoice = create_sales_invoice(do_not_save=1) sales_invoice = create_sales_invoice(do_not_save=1)
sales_invoice.items[0].project = item_project.project_name sales_invoice.items[0].project = item_project.name
sales_invoice.project = project.project_name sales_invoice.project = project.name
sales_invoice.submit() sales_invoice.submit()
expected_values = { expected_values = {
"Debtors - _TC": { "Debtors - _TC": {
"project": project.project_name "project": project.name
}, },
"Sales - _TC": { "Sales - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }

View File

@ -446,7 +446,7 @@ class Subscription(Document):
if not self.generate_invoice_at_period_start: if not self.generate_invoice_at_period_start:
return False return False
if self.is_new_subscription(): if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True return True
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices

View File

@ -59,23 +59,111 @@ def validate_filters(filters):
def get_columns(filters): def get_columns(filters):
return [ return [
_("Payment Document") + ":: 100", {
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", "fieldname": "payment_document",
_("Party Type") + "::100", "label": _("Payment Document Type"),
_("Party") + ":Dynamic Link/Party Type:140", "fieldtype": "Data",
_("Posting Date") + ":Date:100", "width": 100
_("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), },
_("Invoice Posting Date") + ":Date:130", {
_("Payment Due Date") + ":Date:130", "fieldname": "payment_entry",
_("Debit") + ":Currency:120", "label": _("Payment Document"),
_("Credit") + ":Currency:120", "fieldtype": "Dynamic Link",
_("Remarks") + "::150", "options": "payment_document",
_("Age") +":Int:40", "width": 160
"0-30:Currency:100", },
"30-60:Currency:100", {
"60-90:Currency:100", "fieldname": "party_type",
_("90-Above") + ":Currency:100", "label": _("Party Type"),
_("Delay in payment (Days)") + "::150" "fieldtype": "Data",
"width": 100
},
{
"fieldname": "party",
"label": _("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 160
},
{
"fieldname": "posting_date",
"label": _("Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "invoice",
"label": _("Invoice"),
"fieldtype": "Link",
"options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
"width": 160
},
{
"fieldname": "invoice_posting_date",
"label": _("Invoice Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "due_date",
"label": _("Payment Due Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "remarks",
"label": _("Remarks"),
"fieldtype": "Data",
"width": 200
},
{
"fieldname": "age",
"label": _("Age"),
"fieldtype": "Int",
"width": 50
},
{
"fieldname": "range1",
"label": "0-30",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range2",
"label": "30-60",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range3",
"label": "60-90",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range4",
"label": _("90 Above"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "delay_in_payment",
"label": _("Delay in payment (Days)"),
"fieldtype": "Int",
"width": 100
}
] ]
def get_conditions(filters): def get_conditions(filters):

View File

@ -48,7 +48,7 @@ class CropCycle(Document):
def import_disease_tasks(self, disease, start_date): def import_disease_tasks(self, disease, start_date):
disease_doc = frappe.get_doc('Disease', disease) disease_doc = frappe.get_doc('Disease', disease)
self.create_task(disease_doc.treatment_task, self.name, start_date) self.create_task(disease_doc.treatment_task, self.project, start_date)
def create_project(self, period, crop_tasks): def create_project(self, period, crop_tasks):
project = frappe.get_doc({ project = frappe.get_doc({

View File

@ -71,4 +71,4 @@ def check_task_creation():
def check_project_creation(): def check_project_creation():
return True if frappe.db.exists('Project', 'Basil from seed 2017') else False return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False

View File

@ -64,8 +64,8 @@ frappe.ui.form.on("Purchase Order Item", {
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() { setup: function() {
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
'Purchase Receipt': 'Receipt', 'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Invoice', 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier', 'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment', 'Payment Entry': 'Payment',
} }

View File

@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { events: {
onCheckRow: function(data) { onCheckRow: function (data) {
if (!data) return;
const data_doctype = $(
data[2].html
)[0].attributes.getNamedItem("data-doctype").value;
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
row_name = data[2].content; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Supplier") {
row_values = data
if(tree_type == "Supplier" || tree_type == "Item") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} } else if (tree_type == "Item") {
else { row_values = data
row_values = data.slice(3,length-1).map(function (column) { .slice(5, length - 1)
return column.content; .map(function (column) {
}) return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
},500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}); });
} }
} }

View File

@ -35,9 +35,10 @@ def update_last_purchase_rate(doc, is_submit):
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate # update last purchsae rate
if last_purchase_rate: frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
(flt(last_purchase_rate), d.item_code))
def validate_for_items(doc): def validate_for_items(doc):
items = [] items = []

View File

@ -328,6 +328,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@ -353,6 +354,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name target_doc.sales_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@ -233,7 +233,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate 'incoming_rate': p.get("incoming_rate")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -252,7 +252,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate 'incoming_rate': d.get("incoming_rate")
})) }))
return il return il

View File

@ -6,6 +6,7 @@ import unittest
from erpnext.stock.doctype.item.test_item import set_item_variant_settings from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
from six import string_types from six import string_types
@ -56,6 +57,8 @@ def make_quality_inspection_template():
qc = frappe.new_doc("Quality Inspection Template") qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template qc.quality_inspection_template_name = qc_template
create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', { qc.append('item_quality_inspection_parameter', {
"specification": "Moisture", "specification": "Moisture",
"value": "&lt; 5%", "value": "&lt; 5%",

View File

@ -124,21 +124,24 @@ class ProgramEnrollment(Document):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters): def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
if filters.get('program'): if not filters.get('program'):
return frappe.db.sql("""select course, course_name from `tabProgram Course` frappe.msgprint(_("Please select a Program first."))
where parent = %(program)s and course like %(txt)s {match_cond} return []
order by
if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), return frappe.db.sql("""select course, course_name from `tabProgram Course`
idx desc, where parent = %(program)s and course like %(txt)s {match_cond}
`tabProgram Course`.course asc order by
limit {start}, {page_len}""".format( if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
match_cond=get_match_cond(doctype), idx desc,
start=start, `tabProgram Course`.course asc
page_len=page_len), { limit {start}, {page_len}""".format(
"txt": "%{0}%".format(txt), match_cond=get_match_cond(doctype),
"_txt": txt.replace('%', ''), start=start,
"program": filters['program'] page_len=page_len), {
}) "txt": "%{0}%".format(txt),
"_txt": txt.replace('%', ''),
"program": filters['program']
})
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -15,6 +15,22 @@ frappe.ui.form.on('Plaid Settings', {
frm.add_custom_button('Link a new bank account', () => { frm.add_custom_button('Link a new bank account', () => {
new erpnext.integrations.plaidLink(frm); new erpnext.integrations.plaidLink(frm);
}); });
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
freeze: true,
callback: () => {
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
frappe.msgprint({
title: __("Sync Started"),
message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
alert: 1
});
}
});
}).addClass("btn-primary");
} }
} }
}); });

View File

@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True) related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token") access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id account_id = related_bank[0].integration_id
else: else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token") access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None account_id = None
@ -228,13 +227,19 @@ def new_bank_transaction(transaction):
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1: if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) enqueue_synchronization()
for plaid_account in plaid_accounts:
frappe.enqueue( @frappe.whitelist()
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", def enqueue_synchronization():
bank=plaid_account.bank, plaid_accounts = frappe.get_all("Bank Account",
bank_account=plaid_account.name filters={"integration_id": ["!=", ""]},
) fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)

View File

@ -17,6 +17,9 @@
"enable_free_follow_ups", "enable_free_follow_ups",
"max_visits", "max_visits",
"valid_days", "valid_days",
"inpatient_settings_section",
"allow_discharge_despite_unbilled_services",
"do_not_bill_inpatient_encounters",
"healthcare_service_items", "healthcare_service_items",
"inpatient_visit_charge_item", "inpatient_visit_charge_item",
"op_consulting_charge_item", "op_consulting_charge_item",
@ -302,11 +305,28 @@
"fieldname": "enable_free_follow_ups", "fieldname": "enable_free_follow_ups",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Free Follow-ups" "label": "Enable Free Follow-ups"
},
{
"fieldname": "inpatient_settings_section",
"fieldtype": "Section Break",
"label": "Inpatient Settings"
},
{
"default": "0",
"fieldname": "allow_discharge_despite_unbilled_services",
"fieldtype": "Check",
"label": "Allow Discharge Despite Unbilled Healthcare Services"
},
{
"default": "0",
"fieldname": "do_not_bill_inpatient_encounters",
"fieldtype": "Check",
"label": "Do Not Bill Patient Encounters for Inpatients"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-07-08 15:17:21.543218", "modified": "2021-01-13 09:04:35.877700",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Settings", "name": "Healthcare Settings",

View File

@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) { refresh: function(frm) {
// Ignore cancellation of doctype on cancel all // Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry']; frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => { frm.set_query('item_code', () => {
return { return {

View File

@ -139,7 +139,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Inpatient Medication Orders", "label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail", "options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@ -180,7 +179,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-03 13:22:37.820707", "modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Inpatient Medication Entry", "name": "Inpatient Medication Entry",

View File

@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document):
self.validate_medication_orders() self.validate_medication_orders()
def get_medication_orders(self): def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters # pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self) orders = get_pending_medication_orders(self)
@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document):
self.set('medication_orders', []) self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria')) frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders): def add_mo_to_table(self, orders):
# Add medication orders in the child table # Add medication orders in the child table
self.set('medication_orders', []) self.set('medication_orders', [])

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ from frappe import _
from frappe.utils import today, now_datetime, getdate, get_datetime from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
@ -113,6 +113,7 @@ def schedule_inpatient(args):
inpatient_record.status = 'Admission Scheduled' inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
def schedule_discharge(args): def schedule_discharge(args):
discharge_order = json.loads(args) discharge_order = json.loads(args)
@ -126,16 +127,19 @@ def schedule_discharge(args):
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
def set_details_from_ip_order(inpatient_record, ip_order): def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order: for key in ip_order:
inpatient_record.set(key, ip_order[key]) inpatient_record.set(key, ip_order[key])
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child: for item in encounter_child:
table = inpatient_record.append(inpatient_record_child) table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'): for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname)) table.set(df.fieldname, item.get(df.fieldname))
def check_out_inpatient(inpatient_record): def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record):
inpatient_occupancy.check_out = now_datetime() inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
def discharge_patient(inpatient_record): def discharge_patient(inpatient_record):
validate_invoiced_inpatient(inpatient_record) validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today() inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged" inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
def validate_invoiced_inpatient(inpatient_record):
pending_invoices = [] def validate_inpatient_invoicing(inpatient_record):
if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
return
pending_invoices = get_pending_invoices(inpatient_record)
if pending_invoices:
message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
formatted_doc_rows = ''
for doctype, docnames in pending_invoices.items():
formatted_doc_rows += """
<td>{0}</td>
<td>{1}</td>
</tr>""".format(doctype, docnames)
message += """
<table class='table'>
<thead>
<th>{0}</th>
<th>{1}</th>
</thead>
{2}
</table>
""".format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
def get_pending_invoices(inpatient_record):
pending_invoices = {}
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
service_unit_names = False service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
if inpatient_occupancy.invoiced != 1: if not inpatient_occupancy.invoiced:
if service_unit_names: if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit service_unit_names += ", " + inpatient_occupancy.service_unit
else: else:
service_unit_names = inpatient_occupancy.service_unit service_unit_names = inpatient_occupancy.service_unit
if service_unit_names: if service_unit_names:
pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")") pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"] docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs: for doc in docs:
doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record) doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list: if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
if pending_invoices: return pending_invoices
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
.join(pending_invoices)), title=_('Unbilled Invoices'))
def get_pending_doc(doc, doc_name_list, pending_invoices): def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list: if doc_name_list:
doc_ids = False doc_ids = False
for doc_name in doc_name_list: for doc_name in doc_name_list:
doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids: if doc_ids:
doc_ids += ", "+doc_name.name doc_ids += ", " + doc_link
else: else:
doc_ids = doc_name.name doc_ids = doc_link
if doc_ids: if doc_ids:
pending_invoices.append(doc + " (" + doc_ids + ")") pending_invoices[doc] = doc_ids
return pending_invoices return pending_invoices
def get_inpatient_docs_not_invoiced(doc, inpatient_record):
def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted' inpatient_record.status = 'Admitted'
@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
def transfer_patient(inpatient_record, service_unit, check_in): def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {}) item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit item_line.service_unit = service_unit
@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in):
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
def patient_leave_service_unit(inpatient_record, check_out, leave_from): def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from):
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters): def get_leave_from(doctype, txt, searchfield, start, page_len, filters):

View File

@ -8,6 +8,8 @@ import unittest
from frappe.utils import now_datetime, today from frappe.utils import now_datetime, today
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter
from erpnext.healthcare.utils import get_encounters_to_invoice
class TestInpatientRecord(unittest.TestCase): class TestInpatientRecord(unittest.TestCase):
def test_admit_and_discharge(self): def test_admit_and_discharge(self):
@ -40,6 +42,60 @@ class TestInpatientRecord(unittest.TestCase):
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
def test_allow_discharge_despite_unbilled_services(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1)
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
# Discharge
schedule_discharge(frappe.as_json({"patient": patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
# Should not validate Pending Invoices
ip_record.discharge()
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0)
def test_do_not_bill_patient_encounters_for_inpatients(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1)
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
# Patient Encounter
patient_encounter = create_patient_encounter()
encounters = get_encounters_to_invoice(patient, "_Test Company")
encounter_ids = [entry.reference_name for entry in encounters]
self.assertFalse(patient_encounter.name in encounter_ids)
# Discharge
schedule_discharge(frappe.as_json({"patient": patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record)
discharge_patient(ip_record)
setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0)
def test_validate_overlap_admission(self): def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient() patient = create_patient()
@ -63,6 +119,13 @@ def mark_invoiced_inpatient_occupancy(ip_record):
inpatient_occupancy.invoiced = 1 inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
def setup_inpatient_settings(key, value):
settings = frappe.get_single("Healthcare Settings")
settings.set(key, value)
settings.save()
def create_inpatient(patient): def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient) patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record') inpatient_record = frappe.new_doc('Inpatient Record')
@ -78,6 +141,7 @@ def create_inpatient(patient):
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
return inpatient_record return inpatient_record
def get_healthcare_service_unit(): def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
if not service_unit: if not service_unit:
@ -105,6 +169,7 @@ def get_healthcare_service_unit():
return service_unit.name return service_unit.name
return service_unit return service_unit
def get_service_unit_type(): def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
@ -116,6 +181,7 @@ def get_service_unit_type():
return service_unit_type.name return service_unit_type.name
return service_unit_type return service_unit_type
def create_patient(): def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient') patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient: if not patient:

View File

@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEquals(appointment.status, 'Open') self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled') self.assertEquals(appointment.status, 'Scheduled')
create_encounter(appointment) encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
encounter.cancel()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self): def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -5,10 +5,10 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate, flt from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
class TestTherapyPlan(unittest.TestCase): class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self): def test_creation_on_encounter_submission(self):
@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit() frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session)
session.submit()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
session.cancel()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_therapy_plan_from_template(self): def test_therapy_plan_from_template(self):
patient = create_patient() patient = create_patient()
template = create_therapy_plan_template() template = create_therapy_plan_template()

View File

@ -47,7 +47,7 @@ class TherapyPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def make_therapy_session(therapy_plan, patient, therapy_type, company): def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
therapy_type = frappe.get_doc('Therapy Type', therapy_type) therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session') therapy_session = frappe.new_doc('Therapy Session')
@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company):
therapy_session.duration = therapy_type.default_duration therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test: if frappe.flags.in_test:
therapy_session.start_date = today() therapy_session.start_date = today()

View File

@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', {
} }
}; };
}); });
frm.set_query('appointment', function() {
return {
filters: {
'status': ['in', ['Open', 'Scheduled']]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -43,7 +43,14 @@ class TherapySession(Document):
self.update_sessions_count_in_therapy_plan() self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self) insert_session_medical_record(self)
def on_update(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
def on_cancel(self): def on_cancel(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
self.update_sessions_count_in_therapy_plan(on_cancel=True) self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False): def update_sessions_count_in_therapy_plan(self, on_cancel=False):

View File

@ -77,11 +77,13 @@ def get_appointments_to_invoice(patient, company):
def get_encounters_to_invoice(patient, company): def get_encounters_to_invoice(patient, company):
if not isinstance(patient, str):
patient = patient.name
encounters_to_invoice = [] encounters_to_invoice = []
encounters = frappe.get_list( encounters = frappe.get_list(
'Patient Encounter', 'Patient Encounter',
fields=['*'], fields=['*'],
filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1}
) )
if encounters: if encounters:
for encounter in encounters: for encounter in encounters:
@ -90,6 +92,10 @@ def get_encounters_to_invoice(patient, company):
income_account = None income_account = None
service_item = None service_item = None
if encounter.practitioner: if encounter.practitioner:
if encounter.inpatient_record and \
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
income_account = get_income_account(encounter.practitioner, encounter.company) income_account = get_income_account(encounter.practitioner, encounter.company)

View File

@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-10-16 15:02:04.283657", "modified": "2021-01-01 16:54:33.477439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",
@ -855,7 +855,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"search_fields": "employee_name", "search_fields": "employee_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@ -38,7 +38,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
onboarding.insert() onboarding.insert()
onboarding.submit() onboarding.submit()
self.assertEqual(onboarding.project, 'Employee Onboarding : Test Researcher - test@researcher.com') project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete # don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)

View File

@ -20,35 +20,36 @@ class TestExpenseClaim(unittest.TestCase):
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("update `tabExpense Claim` set project = '', task = ''") frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
frappe.get_doc({ project = frappe.get_doc({
"project_name": "_Test Project 1", "project_name": "_Test Project 1",
"doctype": "Project" "doctype": "Project"
}).save() })
project.save()
task = frappe.get_doc(dict( task = frappe.get_doc(dict(
doctype = 'Task', doctype = 'Task',
subject = '_Test Project Task 1', subject = '_Test Project Task 1',
status = 'Open', status = 'Open',
project = '_Test Project 1' project = project.name
)).insert() )).insert()
task_name = task.name task_name = task.name
payable_account = get_payable_account(company_name) payable_account = get_payable_account(company_name)
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name) make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, 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", project.name, "total_expense_claim"), 200)
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name) expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, 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", project.name, "total_expense_claim"), 700)
expense_claim2.cancel() expense_claim2.cancel()
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", project.name, "total_expense_claim"), 200)
def test_expense_claim_status(self): def test_expense_claim_status(self):
payable_account = get_payable_account(company_name) payable_account = get_payable_account(company_name)

View File

@ -11,15 +11,24 @@
"field_order": [ "field_order": [
"applicant_name", "applicant_name",
"email_id", "email_id",
"phone_number",
"country",
"status", "status",
"column_break_3", "column_break_3",
"job_title", "job_title",
"source", "source",
"source_name", "source_name",
"applicant_rating",
"section_break_6", "section_break_6",
"notes", "notes",
"cover_letter", "cover_letter",
"resume_attachment" "resume_attachment",
"resume_link",
"section_break_16",
"currency",
"column_break_18",
"lower_range",
"upper_range"
], ],
"fields": [ "fields": [
{ {
@ -91,12 +100,65 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Notes", "label": "Notes",
"read_only": 1 "read_only": 1
},
{
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number",
"options": "Phone"
},
{
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country"
},
{
"fieldname": "resume_link",
"fieldtype": "Data",
"label": "Resume Link"
},
{
"fieldname": "applicant_rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Applicant Rating"
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Salary Expectation"
},
{
"fieldname": "lower_range",
"fieldtype": "Currency",
"label": "Lower Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "upper_range",
"fieldtype": "Currency",
"label": "Upper Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-01-13 16:19:39.113330", "modified": "2020-09-18 12:39:02.557563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Applicant", "name": "Job Applicant",

View File

@ -1,456 +1,188 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "autoname": "field:route",
"allow_import": 0, "creation": "2013-01-15 16:13:36",
"allow_rename": 0, "description": "Description of a Job Opening",
"autoname": "field:route", "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2013-01-15 16:13:36", "engine": "InnoDB",
"custom": 0, "field_order": [
"description": "Description of a Job Opening", "job_title",
"docstatus": 0, "company",
"doctype": "DocType", "status",
"document_type": "Document", "column_break_5",
"editable_grid": 0, "designation",
"engine": "InnoDB", "department",
"staffing_plan",
"planned_vacancies",
"section_break_6",
"publish",
"route",
"column_break_12",
"job_application_route",
"section_break_14",
"description",
"section_break_16",
"currency",
"lower_range",
"upper_range",
"column_break_20",
"publish_salary_range"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "job_title",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Job Title",
"columns": 0, "reqd": 1
"fieldname": "job_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": "Job Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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, "fieldname": "company",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Company",
"collapsible": 0, "options": "Company",
"columns": 0, "reqd": 1
"fieldname": "company", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"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, "fieldname": "status",
"allow_on_submit": 0, "fieldtype": "Select",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "in_standard_filter": 1,
"columns": 0, "label": "Status",
"fieldname": "status", "options": "Open\nClosed"
"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": 1,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "Open\nClosed",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_5",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "designation",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Designation",
"collapsible": 0, "options": "Designation",
"columns": 0, "reqd": 1
"fieldname": "designation", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Designation",
"length": 0,
"no_copy": 0,
"options": "Designation",
"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, "fieldname": "department",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Department",
"collapsible": 0, "options": "Department"
"columns": 0, },
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "staffing_plan",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Staffing Plan",
"collapsible": 0, "options": "Staffing Plan",
"columns": 0, "read_only": 1
"fieldname": "staffing_plan", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Staffing Plan",
"length": 0,
"no_copy": 0,
"options": "Staffing Plan",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "depends_on": "staffing_plan",
"allow_on_submit": 0, "fieldname": "planned_vacancies",
"bold": 0, "fieldtype": "Int",
"collapsible": 0, "label": "Planned number of Positions",
"columns": 0, "read_only": 1
"depends_on": "staffing_plan", },
"fieldname": "planned_vacancies",
"fieldtype": "Int",
"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": "Planned number of Positions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_6",
"allow_on_submit": 0, "fieldtype": "Section Break"
"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, "default": "0",
"allow_on_submit": 0, "fieldname": "publish",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Publish on website"
"columns": 0, },
"fieldname": "publish",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Publish on website",
"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, "depends_on": "publish",
"allow_on_submit": 0, "fieldname": "route",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "label": "Route",
"columns": 0,
"depends_on": "publish",
"fieldname": "route",
"fieldtype": "Data",
"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": "Route",
"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": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0, "description": "Job profile, qualifications required etc.",
"allow_on_submit": 0, "fieldname": "description",
"bold": 0, "fieldtype": "Text Editor",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Description"
"description": "Job profile, qualifications required etc.", },
"fieldname": "description", {
"fieldtype": "Text Editor", "fieldname": "column_break_12",
"hidden": 0, "fieldtype": "Column Break"
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0, {
"in_filter": 0, "fieldname": "section_break_14",
"in_global_search": 0, "fieldtype": "Section Break"
"in_list_view": 1, },
"in_standard_filter": 0, {
"label": "Description", "collapsible": 1,
"length": 0, "fieldname": "section_break_16",
"no_copy": 0, "fieldtype": "Section Break"
"permlevel": 0, },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "currency",
"read_only": 0, "fieldtype": "Link",
"remember_last_selected_value": 0, "label": "Currency",
"report_hide": 0, "options": "Currency"
"reqd": 0, },
"search_index": 0, {
"set_only_once": 0, "fieldname": "lower_range",
"translatable": 0, "fieldtype": "Currency",
"unique": 0 "label": "Lower Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "upper_range",
"fieldtype": "Currency",
"label": "Upper Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"depends_on": "publish",
"description": "Route to the custom Job Application Webform",
"fieldname": "job_application_route",
"fieldtype": "Data",
"label": "Job Application Route"
},
{
"default": "0",
"fieldname": "publish_salary_range",
"fieldtype": "Check",
"label": "Publish Salary Range"
} }
], ],
"has_web_view": 0, "icon": "fa fa-bookmark",
"hide_heading": 0, "idx": 1,
"hide_toolbar": 0, "links": [],
"icon": "fa fa-bookmark", "modified": "2020-09-18 11:23:29.488923",
"idx": 1, "modified_by": "Administrator",
"image_view": 0, "module": "HR",
"in_create": 0, "name": "Job Opening",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-20 15:38:44.705823",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Opening",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 0, "report": 1,
"if_owner": 0, "role": "HR User",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "read": 1,
"cancel": 0, "role": "Guest"
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Guest",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "ASC"
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -43,9 +43,8 @@ class JobOpening(WebsiteGenerator):
current_count = designation_counts['employee_count'] + designation_counts['job_openings'] current_count = designation_counts['employee_count'] + designation_counts['job_openings']
if self.planned_vacancies <= current_count: if self.planned_vacancies <= current_count:
frappe.throw(_("Job Openings for designation {0} already open \ frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
or hiring completed as per Staffing Plan {1}" self.designation, self.staffing_plan))
.format(self.designation, self.staffing_plan)))
def get_context(self, context): def get_context(self, context):
context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
@ -56,7 +55,8 @@ def get_list_context(context):
context.get_list = get_job_openings context.get_list = get_job_openings
def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
fields = ['name', 'status', 'job_title', 'description'] fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range',
'lower_range', 'upper_range', 'currency', 'job_application_route']
filters = filters or {} filters = filters or {}
filters.update({ filters.update({

View File

@ -1,9 +1,18 @@
<div class="my-5"> <div class="my-5">
<h3>{{ doc.job_title }}</h3> <h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p> <p>{{ doc.description }}</p>
{%- if doc.publish_salary_range -%}
<p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
{% endif %}
<div> <div>
<a class="btn btn-primary" {%- if doc.job_application_route -%}
href="/job_application?new=1&job_title={{ doc.name }}"> <a class='btn btn-primary'
href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a> {{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@
"employee", "employee",
"employee_name", "employee_name",
"department", "department",
"company",
"column_break1", "column_break1",
"leave_type", "leave_type",
"from_date", "from_date",
@ -219,6 +220,15 @@
"label": "Leave Policy Assignment", "label": "Leave Policy Assignment",
"options": "Leave Policy Assignment", "options": "Leave Policy Assignment",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
@ -226,7 +236,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-20 14:25:10.314323", "modified": "2021-01-04 18:46:13.184104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-05-09 15:47:39.760406", "creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -8,6 +9,7 @@
"leave_type", "leave_type",
"transaction_type", "transaction_type",
"transaction_name", "transaction_name",
"company",
"leaves", "leaves",
"column_break_7", "column_break_7",
"from_date", "from_date",
@ -106,12 +108,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Holiday List", "label": "Holiday List",
"options": "Holiday List" "options": "Holiday List"
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2020-09-04 12:16:36.569066", "links": [],
"modified": "2021-01-04 18:47:45.146652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Ledger Entry", "name": "Leave Ledger Entry",

View File

@ -1,86 +1,200 @@
{ {
"accept_payment": 0, "accept_payment": 0,
"allow_comments": 1, "allow_comments": 1,
"allow_delete": 0, "allow_delete": 0,
"allow_edit": 1, "allow_edit": 1,
"allow_incomplete": 0, "allow_incomplete": 0,
"allow_multiple": 1, "allow_multiple": 1,
"allow_print": 0, "allow_print": 0,
"amount": 0.0, "amount": 0.0,
"amount_based_on_field": 0, "amount_based_on_field": 0,
"creation": "2016-09-10 02:53:16.598314", "apply_document_permissions": 0,
"doc_type": "Job Applicant", "client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n",
"docstatus": 0, "creation": "2016-09-10 02:53:16.598314",
"doctype": "Web Form", "doc_type": "Job Applicant",
"idx": 0, "docstatus": 0,
"introduction_text": "", "doctype": "Web Form",
"is_standard": 1, "idx": 0,
"login_required": 0, "introduction_text": "",
"max_attachment_size": 0, "is_standard": 1,
"modified": "2016-12-20 00:21:44.081622", "login_required": 0,
"modified_by": "Administrator", "max_attachment_size": 0,
"module": "HR", "modified": "2020-10-07 19:27:17.143355",
"name": "job-application", "modified_by": "Administrator",
"owner": "Administrator", "module": "HR",
"published": 1, "name": "job-application",
"route": "job_application", "owner": "Administrator",
"show_sidebar": 1, "published": 1,
"sidebar_items": [], "route": "job_application",
"success_message": "Thank you for applying.", "route_to_success_link": 0,
"success_url": "/jobs", "show_attachments": 0,
"title": "Job Application", "show_in_grid": 0,
"show_sidebar": 1,
"sidebar_items": [],
"success_message": "Thank you for applying.",
"success_url": "/jobs",
"title": "Job Application",
"web_form_fields": [ "web_form_fields": [
{ {
"fieldname": "job_title", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "job_title",
"hidden": 0, "fieldtype": "Data",
"label": "Job Opening", "hidden": 0,
"max_length": 0, "label": "Job Opening",
"max_value": 0, "max_length": 0,
"options": "", "max_value": 0,
"read_only": 1, "options": "",
"reqd": 0 "read_only": 1,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "applicant_name", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "applicant_name",
"hidden": 0, "fieldtype": "Data",
"label": "Applicant Name", "hidden": 0,
"max_length": 0, "label": "Applicant Name",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 1 "read_only": 0,
}, "reqd": 1,
"show_in_filter": 0
},
{ {
"fieldname": "email_id", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "email_id",
"hidden": 0, "fieldtype": "Data",
"label": "Email Address", "hidden": 0,
"max_length": 0, "label": "Email Address",
"max_value": 0, "max_length": 0,
"options": "Email", "max_value": 0,
"read_only": 0, "options": "Email",
"reqd": 1 "read_only": 0,
}, "reqd": 1,
"show_in_filter": 0
},
{ {
"fieldname": "cover_letter", "allow_read_on_all_link_options": 0,
"fieldtype": "Text", "fieldname": "phone_number",
"hidden": 0, "fieldtype": "Data",
"label": "Cover Letter", "hidden": 0,
"max_length": 0, "label": "Phone Number",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "options": "Phone",
}, "read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "resume_attachment", "allow_read_on_all_link_options": 0,
"fieldtype": "Attach", "fieldname": "country",
"hidden": 0, "fieldtype": "Link",
"label": "Resume Attachment", "hidden": 0,
"max_length": 0, "label": "Country of Residence",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "options": "Country",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "cover_letter",
"fieldtype": "Text",
"hidden": 0,
"label": "Cover Letter",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "resume_link",
"fieldtype": "Data",
"hidden": 0,
"label": "Resume Link",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Section Break",
"hidden": 0,
"label": "Expected Salary Range per month",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 0,
"label": "Currency",
"max_length": 0,
"max_value": 0,
"options": "Currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "lower_range",
"fieldtype": "Currency",
"hidden": 0,
"label": "Lower Range",
"max_length": 0,
"max_value": 0,
"options": "currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "upper_range",
"fieldtype": "Currency",
"hidden": 0,
"label": "Upper Range",
"max_length": 0,
"max_value": 0,
"options": "currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
} }
] ]
} }

View File

@ -362,6 +362,27 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db() unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1) self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
unpledge_map = {'Test Security 1': 4000}
unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
unpledge_request.submit()
unpledge_request.status = 'Approved'
unpledge_request.save()
unpledge_request.submit()
def test_disbursal_check_with_shortfall(self): def test_disbursal_check_with_shortfall(self):
pledges = [{ pledges = [{
"loan_security": "Test Security 2", "loan_security": "Test Security 2",

View File

@ -44,10 +44,16 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime()) "valid_upto": (">=", get_datetime())
}, as_list=1)) }, as_list=1))
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable', 'written_off_amount']) 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
else:
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0 security_value = 0
unpledge_qty_map = {} unpledge_qty_map = {}
ltv_ratio = 0 ltv_ratio = 0

View File

@ -103,7 +103,7 @@ def get_data(filters):
loan_repayments = frappe.get_all("Loan Repayment", loan_repayments = frappe.get_all("Loan Repayment",
filters = query_filters, filters = query_filters,
fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount", fields=["posting_date", "applicant", "name", "against_loan", "payable_amount",
"pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
) )

View File

@ -411,7 +411,7 @@ cur_frm.cscript.hour_rate = function(doc) {
cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate;
cur_frm.cscript.bom_no = function(doc, cdt, cdn) { cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
get_bom_material_detail(doc, cdt, cdn, false); get_bom_material_detail(doc, cdt, cdn, false);
}; };
@ -419,17 +419,22 @@ cur_frm.cscript.is_default = function(doc) {
if (doc.is_default) cur_frm.set_value("is_active", 1); if (doc.is_default) cur_frm.set_value("is_active", 1);
}; };
var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
if (!doc.company) {
frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
}
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if (d.item_code) { if (d.item_code) {
return frappe.call({ return frappe.call({
doc: doc, doc: doc,
method: "get_bom_material_detail", method: "get_bom_material_detail",
args: { args: {
'item_code': d.item_code, "company": doc.company,
'bom_no': d.bom_no != null ? d.bom_no: '', "item_code": d.item_code,
"bom_no": d.bom_no != null ? d.bom_no: '',
"scrap_items": scrap_items, "scrap_items": scrap_items,
'qty': d.qty, "qty": d.qty,
"stock_qty": d.stock_qty, "stock_qty": d.stock_qty,
"include_item_in_manufacturing": d.include_item_in_manufacturing, "include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom, "uom": d.uom,
@ -468,7 +473,7 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) {
} }
if (d.bom_no) { if (d.bom_no) {
frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item")); frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
get_bom_material_detail(doc, cdt, cdn, scrap_items); get_bom_material_detail(doc, cdt, cdn, scrap_items);
} else { } else {
erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_rm_cost(doc);

View File

@ -65,6 +65,10 @@ class BOM(WebsiteGenerator):
def validate(self): def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-') self.route = frappe.scrub(self.name).replace('_', '-')
if not self.company:
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations() self.clear_operations()
self.validate_main_item() self.validate_main_item()
self.validate_currency() self.validate_currency()
@ -125,6 +129,7 @@ class BOM(WebsiteGenerator):
self.validate_bom_currecny(item) self.validate_bom_currecny(item)
ret = self.get_bom_material_detail({ ret = self.get_bom_material_detail({
"company": self.company,
"item_code": item.item_code, "item_code": item.item_code,
"item_name": item.item_name, "item_name": item.item_name,
"bom_no": item.bom_no, "bom_no": item.bom_no,
@ -213,6 +218,7 @@ class BOM(WebsiteGenerator):
for d in self.get("items"): for d in self.get("items"):
rate = self.get_rm_rate({ rate = self.get_rm_rate({
"company": self.company,
"item_code": d.item_code, "item_code": d.item_code,
"bom_no": d.bom_no, "bom_no": d.bom_no,
"qty": d.qty, "qty": d.qty,
@ -611,10 +617,20 @@ def get_valuation_rate(args):
""" Get weighted average of valuation rate from all warehouses """ """ Get weighted average of valuation rate from all warehouses """
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` item_bins = frappe.db.sql("""
where item_code=%s""", args['item_code'], as_dict=1): select
total_qty += flt(d.actual_qty) bin.actual_qty, bin.stock_value
total_value += flt(d.stock_value) from
`tabBin` bin, `tabWarehouse` warehouse
where
bin.item_code=%(item)s
and bin.warehouse = warehouse.name
and warehouse.company=%(company)s""",
{"item": args['item_code'], "company": args['company']}, as_dict=1)
for d in item_bins:
total_qty += flt(d.actual_qty)
total_value += flt(d.stock_value)
if total_qty: if total_qty:
valuation_rate = total_value / total_qty valuation_rate = total_value / total_qty

View File

@ -456,10 +456,10 @@ class WorkOrder(Document):
if data and len(data): if data and len(data):
dates = [d.posting_datetime for d in data] dates = [d.posting_datetime for d in data]
self.actual_start_date = min(dates) self.db_set('actual_start_date', min(dates))
if self.status == "Completed": if self.status == "Completed":
self.actual_end_date = max(dates) self.db_set('actual_end_date', max(dates))
self.set_lead_time() self.set_lead_time()
@ -725,6 +725,7 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
args.update(item_data) args.update(item_data)
args["rate"] = get_bom_item_rate({ args["rate"] = get_bom_item_rate({
"company": wo_doc.company,
"item_code": args.get("item_code"), "item_code": args.get("item_code"),
"qty": args.get("required_qty"), "qty": args.get("required_qty"),
"uom": args.get("stock_uom"), "uom": args.get("stock_uom"),

View File

@ -20,6 +20,7 @@ def get_columns():
_("Item") + ":Link/Item:150", _("Item") + ":Link/Item:150",
_("Description") + "::300", _("Description") + "::300",
_("BOM Qty") + ":Float:160", _("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160",
_("Required Qty") + ":Float:120", _("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120", _("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200", _("Enough Parts to Build") + ":Float:200",
@ -32,7 +33,7 @@ def get_bom_stock(filters):
bom = filters.get("bom") bom = filters.get("bom")
table = "`tabBOM Item`" table = "`tabBOM Item`"
qty_field = "qty" qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1) qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0: if int(qty_to_produce) <= 0:
@ -40,7 +41,6 @@ def get_bom_stock(filters):
if filters.get("show_exploded_view"): if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`" table = "`tabBOM Explosion Item`"
qty_field = "stock_qty"
if filters.get("warehouse"): if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@ -59,6 +59,7 @@ def get_bom_stock(filters):
bom_item.item_code, bom_item.item_code,
bom_item.description , bom_item.description ,
bom_item.{qty_field}, bom_item.{qty_field},
bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity, bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty, sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))

View File

@ -742,3 +742,6 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field

View File

@ -0,0 +1,23 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter')
# get all distinct parameters from QI readigs table
reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"])
reading_params = [d.specification for d in reading_params]
# get all distinct parameters from QI Template as some may be unused in QI
template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"])
template_params = [d.specification for d in template_params]
params = list(set(reading_params + template_params))
for parameter in params:
if not frappe.db.exists("Quality Inspection Parameter", parameter):
frappe.get_doc({
"doctype": "Quality Inspection Parameter",
"parameter": parameter,
"description": parameter
}).insert(ignore_permissions=True)

View File

@ -0,0 +1,7 @@
import frappe
def execute():
frappe.reload_doc('HR', 'doctype', 'Leave Allocation')
frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry')
frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""")
frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""")

View File

@ -0,0 +1,44 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("projects", "doctype", "project_template")
frappe.reload_doc("projects", "doctype", "project_template_task")
frappe.reload_doc("projects", "doctype", "project_template")
frappe.reload_doc("projects", "doctype", "task")
for template_name in frappe.db.sql("""
select
name
from
`tabProject Template` """,
as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
for task in template.tasks:
if task.subject:
replace_tasks = True
new_task = frappe.get_doc(dict(
doctype = "Task",
subject = task.subject,
start = task.start,
duration = task.duration,
task_weight = task.task_weight,
description = task.description,
is_template = 1
)).insert()
new_tasks.append(new_task)
if replace_tasks:
template.tasks = []
for tsk in new_tasks:
template.append("tasks", {
"task": tsk.name,
"subject": tsk.subject
})
template.save()

View File

@ -5,11 +5,11 @@ from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
# udpate sales cycle # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']: for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d) frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
# udpate purchase cycle # update purchase cycle
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']: for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d) frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)

View File

@ -49,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', {
} }
).toggleClass('btn-primary', !(frm.doc.employees || []).length); ).toggleClass('btn-primary', !(frm.doc.employees || []).length);
} }
if ((frm.doc.employees || []).length) { if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
frm.page.clear_primary_action(); frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit').then(() => { frm.save('Submit').then(() => {

View File

@ -21,6 +21,9 @@ class PayrollEntry(Document):
if cint(entries) == len(self.employees): if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True) self.set_onload("submitted_ss", True)
def validate(self):
self.number_of_employees = len(self.employees)
def on_submit(self): def on_submit(self):
self.create_salary_slips() self.create_salary_slips()
@ -113,7 +116,7 @@ class PayrollEntry(Document):
for d in employees: for d in employees:
self.append('employees', d) self.append('employees', d)
self.number_of_employees = len(employees) self.number_of_employees = len(self.employees)
if self.validate_attendance: if self.validate_attendance:
return self.validate_employee_attendance() return self.validate_employee_attendance()
@ -145,8 +148,8 @@ class PayrollEntry(Document):
""" """
self.check_permission('write') self.check_permission('write')
self.created = 1 self.created = 1
emp_list = [d.employee for d in self.get_emp_list()] employees = [emp.employee for emp in self.employees]
if emp_list: if employees:
args = frappe._dict({ args = frappe._dict({
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
"payroll_frequency": self.payroll_frequency, "payroll_frequency": self.payroll_frequency,
@ -160,10 +163,10 @@ class PayrollEntry(Document):
"exchange_rate": self.exchange_rate, "exchange_rate": self.exchange_rate,
"currency": self.currency "currency": self.currency
}) })
if len(emp_list) > 30: if len(employees) > 30:
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
else: else:
create_salary_slips_for_employees(emp_list, args, publish_progress=False) create_salary_slips_for_employees(employees, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually # since this method is called via frm.call this doc needs to be updated manually
self.reload() self.reload()

View File

@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.sql("delete from `tab%s`" % dt) frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
"_Test Payroll Payable - _TC") "_Test Payroll Payable - _TC")
currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
dates = get_start_end_dates('Monthly', nowdate()) dates = get_start_end_dates('Monthly', nowdate())
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee from erpnext.hr.utils import get_holidays_for_employee
@ -88,6 +88,8 @@ def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll
period_start = joining_date period_start = joining_date
if relieving_date and getdate(relieving_date) < getdate(period_end): if relieving_date and getdate(relieving_date) < getdate(period_end):
period_end = relieving_date period_end = relieving_date
if month_diff(period_end, start_date) > 1:
start_date = add_months(start_date, - (month_diff(period_end, start_date)+1))
total_sub_periods, remaining_sub_periods = 0.0, 0.0 total_sub_periods, remaining_sub_periods = 0.0, 0.0

View File

@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", {
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
calculate_totals(frm);
frm.trigger("set_dynamic_labels"); frm.trigger("set_dynamic_labels");
}, },

View File

@ -143,8 +143,8 @@ class SalarySlip(TransactionBase):
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet() self.set_time_sheet()
self.pull_sal_struct() self.pull_sal_struct()
payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
return [payroll_based_on, consider_unmarked_attendance_as] return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
def set_time_sheet(self): def set_time_sheet(self):
if self.salary_slip_based_on_timesheet: if self.salary_slip_based_on_timesheet:
@ -424,16 +424,19 @@ class SalarySlip(TransactionBase):
def calculate_net_pay(self): def calculate_net_pay(self):
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("earnings") self.calculate_component_amounts("earnings")
self.gross_pay = self.get_component_totals("earnings") self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("deductions") self.calculate_component_amounts("deductions")
self.total_deduction = self.get_component_totals("deductions")
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.set_loan_repayment() self.set_loan_repayment()
self.set_component_amounts_based_on_payment_days()
self.set_net_pay()
def set_net_pay(self):
self.total_deduction = self.get_component_totals("deductions")
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay) self.rounded_total = rounded(self.net_pay)
self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
@ -455,8 +458,6 @@ class SalarySlip(TransactionBase):
else: else:
self.add_tax_components(payroll_period) self.add_tax_components(payroll_period)
self.set_component_amounts_based_on_payment_days(component_type)
def add_structure_components(self, component_type): def add_structure_components(self, component_type):
data = self.get_data_for_eval() data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type): for struct_row in self._salary_structure_doc.get(component_type):
@ -576,7 +577,7 @@ class SalarySlip(TransactionBase):
'default_amount': amount if not struct_row.get("is_additional_component") else 0, 'default_amount': amount if not struct_row.get("is_additional_component") else 0,
'depends_on_payment_days' : struct_row.depends_on_payment_days, 'depends_on_payment_days' : struct_row.depends_on_payment_days,
'salary_component' : struct_row.salary_component, 'salary_component' : struct_row.salary_component,
'abbr' : struct_row.abbr, 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
'additional_salary': additional_salary, 'additional_salary': additional_salary,
'do_not_include_in_total' : struct_row.do_not_include_in_total, 'do_not_include_in_total' : struct_row.do_not_include_in_total,
'is_tax_applicable': struct_row.is_tax_applicable, 'is_tax_applicable': struct_row.is_tax_applicable,
@ -813,7 +814,7 @@ class SalarySlip(TransactionBase):
cint(row.depends_on_payment_days) and cint(self.total_working_days) and cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or getdate(self.start_date) < joining_date or
getdate(self.end_date) > relieving_date (relieving_date and getdate(self.end_date) > relieving_date)
)): )):
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount")) / cint(self.total_working_days)), row.precision("additional_amount"))
@ -946,15 +947,21 @@ class SalarySlip(TransactionBase):
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row return struct_row
def get_component_totals(self, component_type): def get_component_totals(self, component_type, depends_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
total = 0.0 total = 0.0
for d in self.get(component_type): for d in self.get(component_type):
if not d.do_not_include_in_total: if not d.do_not_include_in_total:
d.amount = flt(d.amount, d.precision("amount")) if depends_on_payment_days:
total += d.amount amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
else:
amount = flt(d.amount, d.precision("amount"))
total += amount
return total return total
def set_component_amounts_based_on_payment_days(self, component_type): def set_component_amounts_based_on_payment_days(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@ -964,8 +971,9 @@ class SalarySlip(TransactionBase):
if not joining_date: if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for d in self.get(component_type): for component_type in ("earnings", "deductions"):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] for d in self.get(component_type):
d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
def set_loan_repayment(self): def set_loan_repayment(self):
self.total_loan_repayment = 0 self.total_loan_repayment = 0
@ -1089,17 +1097,17 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay() self.calculate_net_pay()
def set_totals(self): def set_totals(self):
self.gross_pay = 0 self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1: if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet() self.calculate_total_for_salary_slip_based_on_timesheet()
else: else:
self.total_deduction = 0 self.total_deduction = 0.0
if self.earnings: if self.earnings:
for earning in self.earnings: for earning in self.earnings:
self.gross_pay += flt(earning.amount) self.gross_pay += flt(earning.amount, earning.precision("amount"))
if self.deductions: if self.deductions:
for deduction in self.deductions: for deduction in self.deductions:
self.total_deduction += flt(deduction.amount) self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
self.set_base_totals() self.set_base_totals()
@ -1145,8 +1153,10 @@ class SalarySlip(TransactionBase):
fields = ['sum(net_pay) as sum'], fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', period_start_date], 'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date]}) 'end_date' : ['<', period_end_date],
'name': ['!=', self.name],
'docstatus': 1
})
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
@ -1160,7 +1170,9 @@ class SalarySlip(TransactionBase):
fields = ['sum(net_pay) as sum'], fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', first_day_of_the_month], 'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date] 'end_date' : ['<', self.start_date],
'name': ['!=', self.name],
'docstatus': 1
}) })
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0

View File

@ -318,7 +318,7 @@ class TestSalarySlip(unittest.TestCase):
year_to_date = 0 year_to_date = 0
for slip in salary_slips: for slip in salary_slips:
year_to_date += slip.net_pay year_to_date += flt(slip.net_pay)
self.assertEqual(slip.year_to_date, year_to_date) self.assertEqual(slip.year_to_date, year_to_date)
def test_tax_for_payroll_period(self): def test_tax_for_payroll_period(self):
@ -585,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"amount": 200, "amount": 200,
"exempted_from_income_tax": 1 "exempted_from_income_tax": 1
},
{
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
} }
] ]
if not test_tax: if not test_tax:
@ -603,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"type": "Deduction", "type": "Deduction",
"round_to_the_nearest_integer": 1 "round_to_the_nearest_integer": 1
}) })
else:
data.append({
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
})
if setup or test_tax: if setup or test_tax:
make_salary_component(data, test_tax, company_list) make_salary_component(data, test_tax, company_list)

View File

@ -2,12 +2,13 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:project_name", "autoname": "naming_series:",
"creation": "2013-03-07 11:55:07", "creation": "2013-03-07 11:55:07",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series",
"project_name", "project_name",
"status", "status",
"project_type", "project_type",
@ -440,13 +441,24 @@
"fieldtype": "Text", "fieldtype": "Text",
"label": "Message", "label": "Message",
"mandatory_depends_on": "collect_progress" "mandatory_depends_on": "collect_progress"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "PROJ-.####",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
} }
], ],
"icon": "fa fa-puzzle-piece", "icon": "fa fa-puzzle-piece",
"idx": 29, "idx": 29,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2020-04-08 22:11:14.552615", "modified": "2020-09-02 11:54:01.223620",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",
@ -488,5 +500,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1 "track_seen": 1
} }

View File

@ -13,6 +13,7 @@ from frappe.desk.reportview import get_match_cond
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
class Project(Document): class Project(Document):
def get_feed(self): def get_feed(self):
@ -54,17 +55,64 @@ class Project(Document):
self.project_type = template.project_type self.project_type = template.project_type
# create tasks from template # create tasks from template
project_tasks = []
tmp_task_details = []
for task in template.tasks: for task in template.tasks:
frappe.get_doc(dict( template_task_details = frappe.get_doc("Task", task.task)
doctype = 'Task', tmp_task_details.append(template_task_details)
subject = task.subject, task = self.create_task_from_template(template_task_details)
project = self.name, project_tasks.append(task)
status = 'Open', self.dependency_mapping(tmp_task_details, project_tasks)
exp_start_date = add_days(self.expected_start_date, task.start),
exp_end_date = add_days(self.expected_start_date, task.start + task.duration), def create_task_from_template(self, task_details):
description = task.description, return frappe.get_doc(dict(
task_weight = task.task_weight doctype = 'Task',
)).insert() subject = task_details.subject,
project = self.name,
status = 'Open',
exp_start_date = self.calculate_start_date(task_details),
exp_end_date = self.calculate_end_date(task_details),
description = task_details.description,
task_weight = task_details.task_weight,
type = task_details.type,
issue = task_details.issue,
is_group = task_details.is_group
)).insert()
def calculate_start_date(self, task_details):
self.start_date = add_days(self.expected_start_date, task_details.start)
self.start_date = update_if_holiday(self.holiday_list, self.start_date)
return self.start_date
def calculate_end_date(self, task_details):
self.end_date = add_days(self.start_date, task_details.duration)
return update_if_holiday(self.holiday_list, self.end_date)
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
project_task = frappe.get_doc("Task", project_task.name)
self.check_depends_on_value(template_task, project_task, project_tasks)
self.check_for_parent_tasks(template_task, project_task, project_tasks)
def check_depends_on_value(self, template_task, project_task, project_tasks):
if template_task.get("depends_on") and not project_task.get("depends_on"):
for child_task in template_task.get("depends_on"):
child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
if len(corresponding_project_task):
project_task.append("depends_on",{
"task": corresponding_project_task[0].name
})
project_task.save()
def check_for_parent_tasks(self, template_task, project_task, project_tasks):
if template_task.get("parent_task") and not project_task.get("parent_task"):
parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
if len(corresponding_project_task):
project_task.parent_task = corresponding_project_task[0].name
project_task.save()
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
@ -493,3 +541,9 @@ def set_project_status(project, status):
project.status = status project.status = status
project.save() project.save()
def update_if_holiday(holiday_list, date):
holiday_list = holiday_list or get_holiday_list()
while is_holiday(holiday_list, date):
date = add_days(date, 1)
return date

View File

@ -7,60 +7,131 @@ import frappe, unittest
test_records = frappe.get_test_records('Project') test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"] test_ignore = ["Sales Order"]
from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.project.project import set_project_status from erpnext.projects.doctype.project.project import update_if_holiday
from erpnext.projects.doctype.task.test_task import create_task
from frappe.utils import getdate from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase): class TestProject(unittest.TestCase):
def test_project_with_template(self): def test_project_with_template_having_no_parent_and_depend_tasks(self):
frappe.db.sql('delete from tabTask where project = "Test Project with Template"') project_name = "Test Project with Template - No Parent and Dependend Tasks"
frappe.delete_doc('Project', 'Test Project with Template') frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
project = get_project('Test Project with Template') task1 = task_exists("Test Template Task with No Parent and Dependency")
if not task1:
task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
task1 = tasks[0] self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
self.assertEqual(task1.subject, 'Task 1') self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
self.assertEqual(task1.description, 'Task 1 description') self.assertEqual(len(tasks), 1)
self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
self.assertEqual(len(tasks), 4) def test_project_template_having_parent_child_tasks(self):
task4 = tasks[3] project_name = "Test Project with Template - Tasks with Parent-Child Relation"
self.assertEqual(task4.subject, 'Task 4') frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06')) frappe.delete_doc('Project', project_name)
def get_project(name): task1 = task_exists("Test Template Task Parent")
template = get_project_template() if not task1:
task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
task2 = task_exists("Test Template Task Child 1")
if not task2:
task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
task3 = task_exists("Test Template Task Child 2")
if not task3:
task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
self.assertEqual(tasks[1].parent_task, tasks[0].name)
self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
self.assertEqual(tasks[2].parent_task, tasks[0].name)
self.assertEqual(len(tasks), 3)
def test_project_template_having_dependent_tasks(self):
project_name = "Test Project with Template - Dependent Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
task1 = task_exists("Test Template Task for Dependency")
if not task1:
task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
task2 = task_exists("Test Template Task with Dependency")
if not task2:
task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
self.assertEqual(len(tasks), 2)
def get_project(name, template):
project = frappe.get_doc(dict( project = frappe.get_doc(dict(
doctype = 'Project', doctype = 'Project',
project_name = name, project_name = name,
status = 'Open', status = 'Open',
project_template = template.name, project_template = template.name,
expected_start_date = '2019-01-01' expected_start_date = nowdate()
)).insert() )).insert()
return project return project
def make_project(args): def make_project(args):
args = frappe._dict(args) args = frappe._dict(args)
if args.project_template_name:
template = make_project_template(args.project_template_name) if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}):
else: return frappe.get_doc("Project", {"project_name": args.project_name})
template = get_project_template()
project = frappe.get_doc(dict( project = frappe.get_doc(dict(
doctype = 'Project', doctype = 'Project',
project_name = args.project_name, project_name = args.project_name,
status = 'Open', status = 'Open',
project_template = template.name,
expected_start_date = args.start_date expected_start_date = args.start_date
)) ))
if not frappe.db.exists("Project", args.project_name): if args.project_template_name:
project.insert() template = make_project_template(args.project_template_name)
project.project_template = template.name
return project project.insert()
return project
def task_exists(subject):
result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
if not len(result):
return False
return frappe.get_doc("Task", result[0].name)
def calculate_end_date(project, start, duration):
start = add_days(project.expected_start_date, start)
start = update_if_holiday(project.holiday_list, start)
end = add_days(start, duration)
end = update_if_holiday(project.holiday_list, end)
return getdate(end)

View File

@ -5,4 +5,23 @@ frappe.ui.form.on('Project Template', {
// refresh: function(frm) { // refresh: function(frm) {
// } // }
setup: function (frm) {
frm.set_query("task", "tasks", function () {
return {
filters: {
"is_template": 1
}
};
});
}
});
frappe.ui.form.on('Project Template Task', {
task: function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.db.get_value("Task", row.task, "subject", (value) => {
row.subject = value.subject;
refresh_field("tasks");
});
}
}); });

View File

@ -3,8 +3,28 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _
from frappe.utils import get_link_to_form
class ProjectTemplate(Document): class ProjectTemplate(Document):
pass
def validate(self):
self.validate_dependencies()
def validate_dependencies(self):
for task in self.tasks:
task_details = frappe.get_doc("Task", task.task)
if task_details.depends_on:
for dependency_task in task_details.depends_on:
if not self.check_dependent_task_presence(dependency_task.task):
task_details_format = get_link_to_form("Task",task_details.name)
dependency_task_format = get_link_to_form("Task", dependency_task.task)
frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
def check_dependent_task_presence(self, task):
for task_details in self.tasks:
if task_details.task == task:
return True
return False

View File

@ -5,44 +5,25 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from erpnext.projects.doctype.task.test_task import create_task
class TestProjectTemplate(unittest.TestCase): class TestProjectTemplate(unittest.TestCase):
pass pass
def get_project_template():
if not frappe.db.exists('Project Template', 'Test Project Template'):
frappe.get_doc(dict(
doctype = 'Project Template',
name = 'Test Project Template',
tasks = [
dict(subject='Task 1', description='Task 1 description',
start=0, duration=3),
dict(subject='Task 2', description='Task 2 description',
start=0, duration=2),
dict(subject='Task 3', description='Task 3 description',
start=2, duration=4),
dict(subject='Task 4', description='Task 4 description',
start=3, duration=2),
]
)).insert()
return frappe.get_doc('Project Template', 'Test Project Template')
def make_project_template(project_template_name, project_tasks=[]): def make_project_template(project_template_name, project_tasks=[]):
if not frappe.db.exists('Project Template', project_template_name): if not frappe.db.exists('Project Template', project_template_name):
frappe.get_doc(dict( project_tasks = project_tasks or [
doctype = 'Project Template', create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
name = project_template_name, create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
tasks = project_tasks or [
dict(subject='Task 1', description='Task 1 description',
start=0, duration=3),
dict(subject='Task 2', description='Task 2 description',
start=0, duration=2),
dict(subject='Task 3', description='Task 3 description',
start=2, duration=4),
dict(subject='Task 4', description='Task 4 description',
start=3, duration=2),
] ]
)).insert() doc = frappe.get_doc(dict(
doctype = 'Project Template',
name = project_template_name
))
for task in project_tasks:
doc.append("tasks",{
"task": task.name
})
doc.insert()
return frappe.get_doc('Project Template', project_template_name) return frappe.get_doc('Project Template', project_template_name)

View File

@ -1,203 +1,41 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-02-18 17:24:41.830096", "creation": "2019-02-18 17:24:41.830096",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"task",
"subject"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fieldname": "task",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Task",
"columns": 0, "options": "Task",
"reqd": 1
},
{
"columns": 6,
"fieldname": "subject", "fieldname": "subject",
"fieldtype": "Data", "fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Subject"
"label": "Subject",
"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": 0,
"fieldname": "start",
"fieldtype": "Int",
"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": "Begin On (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": 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,
"fieldname": "duration",
"fieldtype": "Int",
"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": "Duration (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": 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,
"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": 0,
"in_standard_filter": 0,
"label": "Task Weight",
"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": 1,
"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
} }
], ],
"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, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-02-18 18:30:22.688966", "modified": "2021-01-07 15:13:40.995071",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project Template Task", "name": "Project Template Task",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"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,
"track_views": 0
} }

View File

@ -12,6 +12,7 @@
"issue", "issue",
"type", "type",
"is_group", "is_group",
"is_template",
"column_break0", "column_break0",
"status", "status",
"priority", "priority",
@ -22,9 +23,11 @@
"sb_timeline", "sb_timeline",
"exp_start_date", "exp_start_date",
"expected_time", "expected_time",
"start",
"column_break_11", "column_break_11",
"exp_end_date", "exp_end_date",
"progress", "progress",
"duration",
"is_milestone", "is_milestone",
"sb_details", "sb_details",
"description", "description",
@ -112,7 +115,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled" "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled"
}, },
{ {
"fieldname": "priority", "fieldname": "priority",
@ -360,6 +363,24 @@
"label": "Completed By", "label": "Completed By",
"no_copy": 1, "no_copy": 1,
"options": "User" "options": "User"
},
{
"default": "0",
"fieldname": "is_template",
"fieldtype": "Check",
"label": "Is Template"
},
{
"depends_on": "is_template",
"fieldname": "start",
"fieldtype": "Int",
"label": "Begin On (Days)"
},
{
"depends_on": "is_template",
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration (Days)"
} }
], ],
"icon": "fa fa-check", "icon": "fa fa-check",
@ -367,7 +388,7 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2020-07-03 12:36:04.960457", "modified": "2020-12-28 11:32:58.714991",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task", "name": "Task",

View File

@ -17,291 +17,312 @@ class CircularReferenceError(frappe.ValidationError): pass
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet): class Task(NestedSet):
nsm_parent_field = 'parent_task' nsm_parent_field = 'parent_task'
def get_feed(self): def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.subject) return '{0}: {1}'.format(_(self.status), self.subject)
def get_customer_details(self): def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust: if cust:
ret = {'customer_name': cust and cust[0][0] or ''} ret = {'customer_name': cust and cust[0][0] or ''}
return ret return ret
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_parent_project_dates() self.validate_parent_project_dates()
self.validate_progress() self.validate_progress()
self.validate_status() self.validate_status()
self.update_depends_on() self.update_depends_on()
self.validate_dependencies_for_template_task()
def validate_dates(self): def validate_dates(self):
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
frappe.bold("Expected End Date"))) frappe.bold("Expected End Date")))
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End Date"))) frappe.bold("Actual End Date")))
def validate_parent_project_dates(self): def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test: if not self.project or frappe.flags.in_test:
return return
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
if expected_end_date: if expected_end_date:
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
def validate_status(self): def validate_status(self):
if self.status!=self.get_db_value("status") and self.status == "Completed": if self.is_template and self.status != "Template":
for d in self.depends_on: self.status = "Template"
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): if self.status!=self.get_db_value("status") and self.status == "Completed":
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) for d in self.depends_on:
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
close_all_assignments(self.doctype, self.name) close_all_assignments(self.doctype, self.name)
def validate_progress(self): def validate_progress(self):
if flt(self.progress or 0) > 100: if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100.")) frappe.throw(_("Progress % for a task cannot be more than 100."))
if flt(self.progress) == 100: if flt(self.progress) == 100:
self.status = 'Completed' self.status = 'Completed'
if self.status == 'Completed': if self.status == 'Completed':
self.progress = 100 self.progress = 100
def update_depends_on(self): def validate_dependencies_for_template_task(self):
depends_on_tasks = self.depends_on_tasks or "" if self.is_template:
for d in self.depends_on: self.validate_parent_template_task()
if d.task and not d.task in depends_on_tasks: self.validate_depends_on_tasks()
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
def update_nsm_model(self): def update_depends_on(self):
frappe.utils.nestedset.update_nsm(self) depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
def on_update(self): def update_nsm_model(self):
self.update_nsm_model() frappe.utils.nestedset.update_nsm(self)
self.check_recursion()
self.reschedule_dependent_tasks()
self.update_project()
self.unassign_todo()
self.populate_depends_on()
def unassign_todo(self): def on_update(self):
if self.status == "Completed": self.update_nsm_model()
close_all_assignments(self.doctype, self.name) self.check_recursion()
if self.status == "Cancelled": self.reschedule_dependent_tasks()
clear(self.doctype, self.name) self.update_project()
self.unassign_todo()
self.populate_depends_on()
def update_total_expense_claim(self): def unassign_todo(self):
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` if self.status == "Completed":
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] close_all_assignments(self.doctype, self.name)
if self.status == "Cancelled":
clear(self.doctype, self.name)
def update_time_and_costing(self): def update_total_expense_claim(self):
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
,self.name, as_dict=1)[0]
if self.status == "Open":
self.status = "Working"
self.total_costing_amount= tl.total_costing_amount
self.total_billing_amount= tl.total_billing_amount
self.actual_time= tl.time
self.act_start_date= tl.start_date
self.act_end_date= tl.end_date
def update_project(self): def update_time_and_costing(self):
if self.project and not self.flags.from_project: tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
frappe.get_cached_doc("Project", self.project).update_project() sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
,self.name, as_dict=1)[0]
if self.status == "Open":
self.status = "Working"
self.total_costing_amount= tl.total_costing_amount
self.total_billing_amount= tl.total_billing_amount
self.actual_time= tl.time
self.act_start_date= tl.start_date
self.act_end_date= tl.end_date
def check_recursion(self): def update_project(self):
if self.flags.ignore_recursion_check: return if self.project and not self.flags.from_project:
check_list = [['task', 'parent'], ['parent', 'task']] frappe.get_cached_doc("Project", self.project).update_project()
for d in check_list:
task_list, count = [self.name], 0
while (len(task_list) > count ):
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
(d[0], d[1], '%s'), cstr(task_list[count]))
count = count + 1
for b in tasks:
if b[0] == self.name:
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
if b[0]:
task_list.append(b[0])
if count == 15: def check_recursion(self):
break if self.flags.ignore_recursion_check: return
check_list = [['task', 'parent'], ['parent', 'task']]
for d in check_list:
task_list, count = [self.name], 0
while (len(task_list) > count ):
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
(d[0], d[1], '%s'), cstr(task_list[count]))
count = count + 1
for b in tasks:
if b[0] == self.name:
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
if b[0]:
task_list.append(b[0])
def reschedule_dependent_tasks(self): if count == 15:
end_date = self.exp_end_date or self.act_end_date break
if end_date:
for task_name in frappe.db.sql("""
select name from `tabTask` as parent
where parent.project = %(project)s
and parent.name in (
select parent from `tabTask Depends On` as child
where child.task = %(task)s and child.project = %(project)s)
""", {'project': self.project, 'task':self.name }, as_dict=1):
task = frappe.get_doc("Task", task_name.name)
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
task.exp_start_date = add_days(end_date, 1)
task.exp_end_date = add_days(task.exp_start_date, task_duration)
task.flags.ignore_recursion_check = True
task.save()
def has_webform_permission(self): def reschedule_dependent_tasks(self):
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") end_date = self.exp_end_date or self.act_end_date
if project_user: if end_date:
return True for task_name in frappe.db.sql("""
select name from `tabTask` as parent
where parent.project = %(project)s
and parent.name in (
select parent from `tabTask Depends On` as child
where child.task = %(task)s and child.project = %(project)s)
""", {'project': self.project, 'task':self.name }, as_dict=1):
task = frappe.get_doc("Task", task_name.name)
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
task.exp_start_date = add_days(end_date, 1)
task.exp_end_date = add_days(task.exp_start_date, task_duration)
task.flags.ignore_recursion_check = True
task.save()
def populate_depends_on(self): def has_webform_permission(self):
if self.parent_task: project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
parent = frappe.get_doc('Task', self.parent_task) if project_user:
if not self.name in [row.task for row in parent.depends_on]: return True
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,
"subject": self.subject
})
parent.save()
def on_trash(self): def populate_depends_on(self):
if check_if_child_exists(self.name): if self.parent_task:
throw(_("Child Task exists for this Task. You can not delete this Task.")) parent = frappe.get_doc('Task', self.parent_task)
if self.name not in [row.task for row in parent.depends_on]:
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,
"subject": self.subject
})
parent.save()
self.update_nsm_model() def on_trash(self):
if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You can not delete this Task."))
def after_delete(self): self.update_nsm_model()
self.update_project()
def update_status(self): def after_delete(self):
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: self.update_project()
from datetime import datetime
if self.exp_end_date < datetime.now().date(): def update_status(self):
self.db_set('status', 'Overdue', update_modified=False) if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
self.update_project() from datetime import datetime
if self.exp_end_date < datetime.now().date():
self.db_set('status', 'Overdue', update_modified=False)
self.update_project()
@frappe.whitelist() @frappe.whitelist()
def check_if_child_exists(name): def check_if_child_exists(name):
child_tasks = frappe.get_all("Task", filters={"parent_task": name}) child_tasks = frappe.get_all("Task", filters={"parent_task": name})
child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks] child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
return child_tasks return child_tasks
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters): def get_project(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
return frappe.db.sql(""" select name from `tabProject` return frappe.db.sql(""" select name from `tabProject`
where %(key)s like %(txt)s where %(key)s like %(txt)s
%(mcond)s %(mcond)s
order by name order by name
limit %(start)s, %(page_len)s""" % { limit %(start)s, %(page_len)s""" % {
'key': searchfield, 'key': searchfield,
'txt': frappe.db.escape('%' + txt + '%'), 'txt': frappe.db.escape('%' + txt + '%'),
'mcond':get_match_cond(doctype), 'mcond':get_match_cond(doctype),
'start': start, 'start': start,
'page_len': page_len 'page_len': page_len
}) })
@frappe.whitelist() @frappe.whitelist()
def set_multiple_status(names, status): def set_multiple_status(names, status):
names = json.loads(names) names = json.loads(names)
for name in names: for name in names:
task = frappe.get_doc("Task", name) task = frappe.get_doc("Task", name)
task.status = status task.status = status
task.save() task.save()
def set_tasks_as_overdue(): def set_tasks_as_overdue():
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
for task in tasks: for task in tasks:
if task.status == "Pending Review": if task.status == "Pending Review":
if getdate(task.review_date) > getdate(today()): if getdate(task.review_date) > getdate(today()):
continue continue
frappe.get_doc("Task", task.name).update_status() frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist() @frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False): def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
def set_missing_values(source, target): def set_missing_values(source, target):
target.append("time_logs", { target.append("time_logs", {
"hours": source.actual_time, "hours": source.actual_time,
"completed": source.status == "Completed", "completed": source.status == "Completed",
"project": source.project, "project": source.project,
"task": source.name "task": source.name
}) })
doclist = get_mapped_doc("Task", source_name, { doclist = get_mapped_doc("Task", source_name, {
"Task": { "Task": {
"doctype": "Timesheet" "doctype": "Timesheet"
} }
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
return doclist return doclist
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False): def get_children(doctype, parent, task=None, project=None, is_root=False):
filters = [['docstatus', '<', '2']] filters = [['docstatus', '<', '2']]
if task: if task:
filters.append(['parent_task', '=', task]) filters.append(['parent_task', '=', task])
elif parent and not is_root: elif parent and not is_root:
# via expand child # via expand child
filters.append(['parent_task', '=', parent]) filters.append(['parent_task', '=', parent])
else: else:
filters.append(['ifnull(`parent_task`, "")', '=', '']) filters.append(['ifnull(`parent_task`, "")', '=', ''])
if project: if project:
filters.append(['project', '=', project]) filters.append(['project', '=', project])
tasks = frappe.get_list(doctype, fields=[ tasks = frappe.get_list(doctype, fields=[
'name as value', 'name as value',
'subject as title', 'subject as title',
'is_group as expandable' 'is_group as expandable'
], filters=filters, order_by='name') ], filters=filters, order_by='name')
# return tasks # return tasks
return tasks return tasks
@frappe.whitelist() @frappe.whitelist()
def add_node(): def add_node():
from frappe.desk.treeview import make_tree_args from frappe.desk.treeview import make_tree_args
args = frappe.form_dict args = frappe.form_dict
args.update({ args.update({
"name_field": "subject" "name_field": "subject"
}) })
args = make_tree_args(**args) args = make_tree_args(**args)
if args.parent_task == 'All Tasks' or args.parent_task == args.project: if args.parent_task == 'All Tasks' or args.parent_task == args.project:
args.parent_task = None args.parent_task = None
frappe.get_doc(args).insert() frappe.get_doc(args).insert()
@frappe.whitelist() @frappe.whitelist()
def add_multiple_tasks(data, parent): def add_multiple_tasks(data, parent):
data = json.loads(data) data = json.loads(data)
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
for d in data: for d in data:
if not d.get("subject"): continue if not d.get("subject"): continue
new_doc['subject'] = d.get("subject") new_doc['subject'] = d.get("subject")
new_task = frappe.get_doc(new_doc) new_task = frappe.get_doc(new_doc)
new_task.insert() new_task.insert()
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Task", ["lft", "rgt"]) frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))

View File

@ -20,7 +20,8 @@ frappe.listview_settings['Task'] = {
"Pending Review": "orange", "Pending Review": "orange",
"Working": "orange", "Working": "orange",
"Completed": "green", "Completed": "green",
"Cancelled": "dark grey" "Cancelled": "dark grey",
"Template": "blue"
} }
return [__(doc.status), colors[doc.status], "status,=," + doc.status]; return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}, },

View File

@ -30,14 +30,16 @@ class TestTask(unittest.TestCase):
}) })
def test_reschedule_dependent_task(self): def test_reschedule_dependent_task(self):
project = frappe.get_value("Project", {"project_name": "_Test Project"})
task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10)) task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10))
task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name) task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name)
task2.get("depends_on")[0].project = "_Test Project" task2.get("depends_on")[0].project = project
task2.save() task2.save()
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
task3.get("depends_on")[0].project = "_Test Project" task3.get("depends_on")[0].project = project
task3.save() task3.save()
task1.update({ task1.update({
@ -97,14 +99,19 @@ class TestTask(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True): def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
if not frappe.db.exists("Task", subject): if not frappe.db.exists("Task", subject):
task = frappe.new_doc('Task') task = frappe.new_doc('Task')
task.status = "Open" task.status = "Open"
task.subject = subject task.subject = subject
task.exp_start_date = start or nowdate() task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate() task.exp_end_date = end or nowdate()
task.project = project or "_Test Project" task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"})
task.is_template = is_template
task.start = begin
task.duration = duration
task.is_group = is_group
task.parent_task = parent_task
if save: if save:
task.save() task.save()
else: else:
@ -116,5 +123,4 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, sa
}) })
if save: if save:
task.save() task.save()
return task return task

View File

@ -89,10 +89,11 @@ class TestTimesheet(unittest.TestCase):
def test_timesheet_billing_based_on_project(self): def test_timesheet_billing_based_on_project(self):
emp = make_employee("test_employee_6@salary.com") emp = make_employee("test_employee_6@salary.com")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice = create_sales_invoice(do_not_save=True)
sales_invoice.project = '_Test Project' sales_invoice.project = project
sales_invoice.submit() sales_invoice.submit()
ts = frappe.get_doc('Timesheet', timesheet.name) ts = frappe.get_doc('Timesheet', timesheet.name)

View File

@ -134,7 +134,7 @@ frappe.ui.form.on("Timesheet", {
}); });
}, },
project: function(frm) { parent_project: function(frm) {
set_project_in_timelog(frm); set_project_in_timelog(frm);
}, },
@ -168,8 +168,8 @@ frappe.ui.form.on("Timesheet Detail", {
}, },
time_logs_add: function(frm, cdt, cdn) { time_logs_add: function(frm, cdt, cdn) {
if(frm.doc.project) { if(frm.doc.parent_project) {
frappe.model.set_value(cdt, cdn, 'project', frm.doc.project); frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
} }
var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
@ -308,7 +308,9 @@ const set_employee_and_company = function(frm) {
}; };
function set_project_in_timelog(frm) { function set_project_in_timelog(frm) {
if(frm.doc.project){ if(frm.doc.parent_project) {
erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project"); $.each(frm.doc.time_logs || [], function(i, item) {
frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project);
});
} }
} }

View File

@ -15,7 +15,7 @@
"column_break_3", "column_break_3",
"salary_slip", "salary_slip",
"status", "status",
"project", "parent_project",
"employee_detail", "employee_detail",
"employee", "employee",
"employee_name", "employee_name",
@ -261,7 +261,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "project", "fieldname": "parent_project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
@ -271,7 +271,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-29 07:50:35.938231", "modified": "2021-01-08 20:51:14.590080",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet", "name": "Timesheet",

View File

@ -537,6 +537,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
company: me.frm.doc.company, company: me.frm.doc.company,
order_type: me.frm.doc.order_type, order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,

View File

@ -24,9 +24,8 @@
}, },
{ {
"fieldname": "reference_invoice", "fieldname": "reference_invoice",
"fieldtype": "Link", "fieldtype": "Data",
"label": "Reference Invoice", "label": "Reference Invoice"
"options": "Sales Invoice"
}, },
{ {
"fieldname": "headers", "fieldname": "headers",
@ -64,7 +63,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-24 21:09:38.882866", "modified": "2021-01-13 12:06:57.253111",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "E Invoice Request Log", "name": "E Invoice Request Log",

View File

@ -7,6 +7,7 @@
"field_order": [ "field_order": [
"enable", "enable",
"section_break_2", "section_break_2",
"sandbox_mode",
"credentials", "credentials",
"auth_token", "auth_token",
"token_expiry" "token_expiry"
@ -41,12 +42,18 @@
"label": "Credentials", "label": "Credentials",
"mandatory_depends_on": "enable", "mandatory_depends_on": "enable",
"options": "E Invoice User" "options": "E Invoice User"
},
{
"default": "0",
"fieldname": "sandbox_mode",
"fieldtype": "Check",
"label": "Sandbox Mode"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-12-22 15:34:57.280044", "modified": "2021-01-13 12:04:49.449199",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "E Invoice Settings", "name": "E Invoice Settings",

View File

@ -48,9 +48,6 @@ def validate_regional(doc):
def missing(field_label, regulation): def missing(field_label, regulation):
"""Notify the user that a required field is missing.""" """Notify the user that a required field is missing."""
context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.' translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
field_label=frappe.bold(_(field_label)), msgprint(formatted_msg)
regulation=regulation
)
)

View File

@ -0,0 +1,12 @@
import frappe
import unittest
from erpnext.regional.germany.accounts_controller import validate_regional
class TestAccountsController(unittest.TestCase):
def setUp(self):
self.sales_invoice = frappe.get_last_doc('Sales Invoice')
def test_validate_regional(self):
validate_regional(self.sales_invoice)

View File

@ -15,7 +15,7 @@ from frappe import _, bold
from pyqrcode import create as qrcreate from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
def validate_einvoice_fields(doc): def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
@ -84,29 +84,32 @@ def get_doc_details(invoice):
)) ))
def get_party_details(address_name): def get_party_details(address_name):
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
gstin = address.get('gstin')
gstin_details = get_gstin_details(gstin) if (not d.gstin
legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') or not d.city
location = gstin_details.get('AddrLoc') or address.get('city') or not d.pincode
state_code = gstin_details.get('StateCode') or not d.address_title
pincode = gstin_details.get('AddrPncd') or not d.address_line1
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) or not d.gst_state_number):
address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
email_id = address.get('email_id')
phone = address.get('phone')
# get last 10 digit
phone = phone.replace(" ", "")[-10:] if phone else ''
if state_code == 97: frappe.throw(
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields')
)
if d.gst_state_number == 97:
# according to einvoice standard # according to einvoice standard
pincode = 999999 pincode = 999999
return frappe._dict(dict( return frappe._dict(dict(
gstin=gstin, legal_name=legal_name, location=location, gstin=d.gstin, legal_name=d.address_title,
pincode=pincode, state_code=state_code, address_line1=address_line1, location=d.city, pincode=d.pincode,
address_line2=address_line2, email=email_id, phone=phone state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
)) ))
def get_gstin_details(gstin): def get_gstin_details(gstin):
@ -127,14 +130,22 @@ def get_gstin_details(gstin):
return GSPConnector.get_gstin_details(gstin) return GSPConnector.get_gstin_details(gstin)
def get_overseas_address_details(address_name): def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( address_title, address_line1, address_line2, city = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
) )
if not address_title or not address_line1 or not city:
frappe.throw(
msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields')
)
return frappe._dict(dict( return frappe._dict(dict(
gstin='URP', legal_name=address_title, address_line1=address_line1, gstin='URP', legal_name=address_title, location=city,
address_line2=address_line2, email=email_id, phone=phone, address_line1=address_line1, address_line2=address_line2,
pincode=999999, state_code=96, place_of_supply=96, location=city pincode=999999, state_code=96, place_of_supply=96
)) ))
def get_item_list(invoice): def get_item_list(invoice):
@ -146,9 +157,10 @@ def get_item_list(invoice):
item.update(d.as_dict()) item.update(d.as_dict())
item.sr_no = d.idx item.sr_no = d.idx
item.discount_amount = abs(item.discount_amount * item.qty) item.description = d.item_name.replace('"', '\\"')
item.description = d.item_name
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty) item.unit_rate = abs(item.base_amount / item.qty)
item.gross_amount = abs(item.base_amount) item.gross_amount = abs(item.base_amount)
item.taxable_value = abs(item.base_amount) item.taxable_value = abs(item.base_amount)
@ -156,6 +168,7 @@ def get_item_list(invoice):
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
item.serial_no = ""
item = update_item_taxes(invoice, item) item = update_item_taxes(invoice, item)
@ -205,8 +218,8 @@ def update_item_taxes(invoice, item):
def get_invoice_value_details(invoice): def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict()) invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(invoice.base_total) invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.discount_amount invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
invoice_value_details.round_off = invoice.rounding_adjustment invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@ -272,7 +285,25 @@ def get_eway_bill_details(invoice):
vehicle_type=vehicle_type[invoice.gst_vehicle_type] vehicle_type=vehicle_type[invoice.gst_vehicle_type]
)) ))
def validate_mandatory_fields(invoice):
if not invoice.company_address:
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
if not invoice.customer_address:
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
title=_('Missing Fields')
)
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
title=_('Missing Fields')
)
def make_einvoice(invoice): def make_einvoice(invoice):
validate_mandatory_fields(invoice)
schema = read_json('einv_template') schema = read_json('einv_template')
transaction_details = get_transaction_details(invoice) transaction_details = get_transaction_details(invoice)
@ -291,7 +322,10 @@ def make_einvoice(invoice):
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
shipping_details = get_party_details(invoice.shipping_address_name) if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
shipping_details = get_party_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount: if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice) payment_details = get_payment_details(invoice)
@ -351,7 +385,7 @@ def validate_einvoice(validations, einvoice, errors=[]):
# remove empty dicts # remove empty dicts
einvoice.pop(fieldname, None) einvoice.pop(fieldname, None)
continue continue
# convert to int or str # convert to int or str
if value_type == 'string': if value_type == 'string':
einvoice[fieldname] = str(value) einvoice[fieldname] = str(value)
@ -383,15 +417,19 @@ class RequestFailed(Exception): pass
class GSPConnector(): class GSPConnector():
def __init__(self, doctype=None, docname=None): def __init__(self, doctype=None, docname=None):
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
sandbox_mode = self.e_invoice_settings.sandbox_mode
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
self.credentials = self.get_credentials() self.credentials = self.get_credentials()
self.base_url = 'https://gsp.adaequare.com' # authenticate url is same for sandbox & live
self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
@ -727,7 +765,7 @@ class GSPConnector():
_file = frappe.new_doc('File') _file = frappe.new_doc('File')
_file.update({ _file.update({
'file_name': f'QRCode_{docname}.png', 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
'attached_to_doctype': doctype, 'attached_to_doctype': doctype,
'attached_to_name': docname, 'attached_to_name': docname,
'content': 'qrcode', 'content': 'qrcode',

View File

@ -7,7 +7,7 @@ import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states from erpnext.regional.india import states
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today from frappe.utils import today
def setup(company=None, patch=True): def setup(company=None, patch=True):
@ -629,15 +629,20 @@ def set_salary_components(docs):
def set_tax_withholding_category(company): def set_tax_withholding_category(company):
accounts = [] accounts = []
fiscal_year = None
abbr = frappe.get_value("Company", company, "abbr") abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
if company and tds_account: if company and tds_account:
accounts = [dict(company=company, account=tds_account)] accounts = [dict(company=company, account=tds_account)]
fiscal_year = get_fiscal_year(today(), company=company)[0] try:
docs = get_tds_details(accounts, fiscal_year) fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
except FiscalYearError:
pass
docs = get_tds_details(accounts, fiscal_year)
for d in docs: for d in docs:
try: try:
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
@ -650,11 +655,14 @@ def set_tax_withholding_category(company):
if accounts: if accounts:
doc.append("accounts", accounts[0]) doc.append("accounts", accounts[0])
# if fiscal year don't match with any of the already entered data, append rate row if fiscal_year:
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] # if fiscal year don't match with any of the already entered data, append rate row
if not fy_exist: fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
doc.append("rates", d.get('rates')[0]) if not fy_exist:
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.save() doc.save()
def set_tds_account(docs, company): def set_tds_account(docs, company):

View File

@ -48,6 +48,9 @@ def validate_gstin_for_india(doc, method):
validate_gstin_check_digit(doc.gstin) validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc) set_gst_state_and_state_number(doc)
if not doc.gst_state:
frappe.throw(_("Please Enter GST state"))
if doc.gst_state_number != doc.gstin[:2]: if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number)) .format(doc.gst_state_number))

View File

@ -7,7 +7,7 @@
frappe.ui.form.on('Quotation', { frappe.ui.form.on('Quotation', {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Sales Order': 'Make Sales Order' 'Sales Order': 'Sales Order'
}, },
frm.set_query("quotation_to", function() { frm.set_query("quotation_to", function() {

View File

@ -25,7 +25,6 @@ class Quotation(SellingController):
def validate(self): def validate(self):
super(Quotation, self).validate() super(Quotation, self).validate()
self.set_status() self.set_status()
self.update_opportunity()
self.validate_uom_is_integer("stock_uom", "qty") self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till() self.validate_valid_till()
self.set_customer_name() self.set_customer_name()
@ -50,21 +49,20 @@ class Quotation(SellingController):
lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"])
self.customer_name = company_name or lead_name self.customer_name = company_name or lead_name
def update_opportunity(self): def update_opportunity(self, status):
for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])):
if opportunity: if opportunity:
self.update_opportunity_status(opportunity) self.update_opportunity_status(status, opportunity)
if self.opportunity: if self.opportunity:
self.update_opportunity_status() self.update_opportunity_status(status)
def update_opportunity_status(self, opportunity=None): def update_opportunity_status(self, status, opportunity=None):
if not opportunity: if not opportunity:
opportunity = self.opportunity opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity) opp = frappe.get_doc("Opportunity", opportunity)
opp.status = None opp.set_status(status=status, update=True)
opp.set_status(update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order(): if not self.has_sales_order():
@ -82,7 +80,7 @@ class Quotation(SellingController):
else: else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
self.update_opportunity() self.update_opportunity('Lost')
self.update_lead() self.update_lead()
self.save() self.save()
@ -95,7 +93,7 @@ class Quotation(SellingController):
self.company, self.base_grand_total, self) self.company, self.base_grand_total, self)
#update enquiry status #update enquiry status
self.update_opportunity() self.update_opportunity('Quotation')
self.update_lead() self.update_lead()
def on_cancel(self): def on_cancel(self):
@ -105,7 +103,7 @@ class Quotation(SellingController):
#update enquiry status #update enquiry status
self.set_status(update=True) self.set_status(update=True)
self.update_opportunity() self.update_opportunity('Open')
self.update_lead() self.update_lead()
def print_other_charges(self,docname): def print_other_charges(self,docname):

View File

@ -158,7 +158,6 @@ class SalesOrder(SellingController):
frappe.throw(_("Quotation {0} is cancelled").format(quotation)) frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True) doc.set_status(update=True)
doc.update_opportunity()
def validate_drop_ship(self): def validate_drop_ship(self):
for d in self.get('items'): for d in self.get('items'):
@ -830,56 +829,49 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers: for supplier in suppliers:
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, {
if len(po) == 0: "Sales Order": {
doc = get_mapped_doc("Sales Order", source_name, { "doctype": "Purchase Order",
"Sales Order": { "field_no_map": [
"doctype": "Purchase Order", "address_display",
"field_no_map": [ "contact_display",
"address_display", "contact_mobile",
"contact_display", "contact_email",
"contact_mobile", "contact_person",
"contact_email", "taxes_and_charges",
"contact_person", "shipping_address",
"taxes_and_charges", "terms"
"shipping_address", ],
"terms" "validation": {
], "docstatus": ["=", 1]
"validation": {
"docstatus": ["=", 1]
}
},
"Sales Order Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_item"],
["parent", "sales_order"],
["stock_uom", "stock_uom"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["delivery_date", "schedule_date"]
],
"field_no_map": [
"rate",
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
} }
}, target_doc, set_missing_values) },
"Sales Order Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_item"],
["parent", "sales_order"],
["stock_uom", "stock_uom"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["delivery_date", "schedule_date"]
],
"field_no_map": [
"rate",
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
}
}, target_doc, set_missing_values)
doc.insert() doc.insert()
else:
suppliers =[]
if suppliers:
frappe.db.commit() frappe.db.commit()
return doc return doc
else:
frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist() @frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None): def make_purchase_order(source_name, selected_items=None, target_doc=None):
@ -1094,4 +1086,4 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
if not total_produced_qty and frappe.flags.in_patch: return if not total_produced_qty and frappe.flags.in_patch: return
frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)

View File

@ -772,6 +772,59 @@ class TestSalesOrder(unittest.TestCase):
so.load_from_db() so.load_from_db()
so.cancel() so.cancel()
def test_drop_shipping_partial_order(self):
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
update_status as so_update_status
# make items
po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
so_items = [
{
"item_code": po_item1.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
},
{
"item_code": po_item2.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
# create so and po
so = make_sales_order(item_list=so_items, do_not_submit=True)
so.submit()
# create po for only one item
po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
po1.submit()
self.assertEqual(so.customer, po1.customer)
self.assertEqual(po1.items[0].sales_order, so.name)
self.assertEqual(po1.items[0].item_code, po_item1.item_code)
#test po item length
self.assertEqual(len(po1.items), 1)
# create po for remaining item
po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
po2.submit()
# teardown
so_update_status("Draft", so.name)
po1.cancel()
po2.cancel()
so.load_from_db()
so.cancel()
def test_reserved_qty_for_closing_so(self): def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"]) fields=["reserved_qty"])

View File

@ -74,67 +74,71 @@ frappe.query_reports["Sales Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { events: {
onCheckRow: function(data) { onCheckRow: function (data) {
if (!data) return;
const data_doctype = $(
data[2].html
)[0].attributes.getNamedItem("data-doctype").value;
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
row_name = data[2].content; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Customer") {
row_values = data
if(tree_type == "Customer") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} else if (tree_type == "Item") { } else if (tree_type == "Item") {
row_values = data.slice(5,length-1).map(function (column) { row_values = data
return column.content; .slice(5, length - 1)
}) .map(function (column) {
} return column.content;
else { });
row_values = data.slice(3,length-1).map(function (column) { } else {
return column.content; row_values = data
}) .slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
}, 500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}) });
}, },
} }

View File

@ -28,7 +28,7 @@ def delete_company_transactions(company_name):
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM", "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
"Item Default"): "Item Default", "Customer", "Supplier"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@ -14,7 +14,7 @@ frappe.ui.form.on("Delivery Note", {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Packing Slip': 'Packing Slip', 'Packing Slip': 'Packing Slip',
'Installation Note': 'Installation Note', 'Installation Note': 'Installation Note',
'Sales Invoice': 'Invoice', 'Sales Invoice': 'Sales Invoice',
'Stock Entry': 'Return', 'Stock Entry': 'Return',
'Shipment': 'Shipment' 'Shipment': 'Shipment'
}, },

View File

@ -8,26 +8,32 @@
"field_order": [ "field_order": [
"specification", "specification",
"value", "value",
"non_numeric",
"column_break_3", "column_break_3",
"min_value",
"max_value",
"formula_based_criteria",
"acceptance_formula" "acceptance_formula"
], ],
"fields": [ "fields": [
{ {
"fieldname": "specification", "fieldname": "specification",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parameter", "label": "Parameter",
"oldfieldname": "specification", "oldfieldname": "specification",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Quality Inspection Parameter",
"print_width": "200px", "print_width": "200px",
"reqd": 1, "reqd": 1,
"width": "200px" "width": "100px"
}, },
{ {
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value", "fieldname": "value",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Acceptance Criteria", "label": "Acceptance Criteria Value",
"oldfieldname": "value", "oldfieldname": "value",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
@ -36,17 +42,45 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 &lt; 10</b>", "depends_on": "formula_based_criteria",
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"in_list_view": 1,
"label": "Acceptance Criteria Formula" "label": "Acceptance Criteria Formula"
},
{
"default": "0",
"fieldname": "formula_based_criteria",
"fieldtype": "Check",
"label": "Formula Based Criteria"
},
{
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"fieldname": "min_value",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Minimum Value"
},
{
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"fieldname": "max_value",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Maximum Value"
},
{
"default": "0",
"fieldname": "non_numeric",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Non-Numeric",
"width": "80px"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-16 16:33:42.421842", "modified": "2021-01-07 21:32:49.866439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Quality Inspection Parameter", "name": "Item Quality Inspection Parameter",

View File

@ -424,6 +424,7 @@ class TestMaterialRequest(unittest.TestCase):
"basic_rate": 1.0 "basic_rate": 1.0
}) })
se_doc.get("items")[1].update({ se_doc.get("items")[1].update({
"item_code": "_Test Item Home Desktop 100",
"qty": 3.0, "qty": 3.0,
"transfer_qty": 3.0, "transfer_qty": 3.0,
"s_warehouse": "_Test Warehouse 1 - _TC", "s_warehouse": "_Test Warehouse 1 - _TC",
@ -534,7 +535,7 @@ class TestMaterialRequest(unittest.TestCase):
mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
uom="_Test UOM 1", conversion_factor=12) uom="_Test UOM 1", conversion_factor=12)
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120) self.assertEqual(requested_qty, existing_requested_qty + 120)

View File

@ -4,6 +4,55 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", { frappe.ui.form.on("Quality Inspection", {
setup: function(frm) {
frm.set_query("batch_no", function() {
return {
filters: {
"item": frm.doc.item_code
}
};
});
// Serial No based on item_code
frm.set_query("item_serial_no", function() {
let filters = {};
if (frm.doc.item_code) {
filters = {
'item_code': frm.doc.item_code
};
}
return { filters: filters };
});
// item code based on GRN/DN
frm.set_query("item_code", function(doc) {
let doctype = doc.reference_type;
if (doc.reference_type !== "Job Card") {
doctype = (doc.reference_type == "Stock Entry") ?
"Stock Entry Detail" : doc.reference_type + " Item";
}
if (doc.reference_type && doc.reference_name) {
let filters = {
"from": doctype,
"inspection_type": doc.inspection_type
};
if (doc.reference_type == doctype)
filters["reference_name"] = doc.reference_name;
else
filters["parent"] = doc.reference_name;
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
filters: filters
};
}
});
},
refresh: function(frm) { refresh: function(frm) {
// Ignore cancellation of reference doctype on cancel all. // Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
@ -31,55 +80,5 @@ frappe.ui.form.on("Quality Inspection", {
} }
}); });
} }
} },
}) });
// item code based on GRN/DN
cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
let doctype = doc.reference_type;
if (doc.reference_type !== "Job Card") {
doctype = (doc.reference_type == "Stock Entry") ?
"Stock Entry Detail" : doc.reference_type + " Item";
}
if (doc.reference_type && doc.reference_name) {
let filters = {
"from": doctype,
"inspection_type": doc.inspection_type
};
if (doc.reference_type == doctype)
filters["reference_name"] = doc.reference_name;
else
filters["parent"] = doc.reference_name;
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
filters: filters
};
}
},
// Serial No based on item_code
cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
var filters = {};
if (doc.item_code) {
filters = {
'item_code': doc.item_code
}
}
return { filters: filters }
}
cur_frm.set_query("batch_no", function(doc) {
return {
filters: {
"item": doc.item_code
}
}
})
cur_frm.add_fetch('item_code', 'item_name', 'item_name');
cur_frm.add_fetch('item_code', 'description', 'description');

View File

@ -136,6 +136,7 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "item_code.item_name",
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@ -143,6 +144,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "item_code.description",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Description", "label": "Description",
@ -236,7 +238,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-19 17:06:05.409963", "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection", "name": "Quality Inspection",

View File

@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details import get_template_details
@ -16,7 +16,7 @@ class QualityInspection(Document):
self.get_item_specification_details() self.get_item_specification_details()
if self.readings: if self.readings:
self.set_status_based_on_acceptance_formula() self.inspect_and_set_status()
def get_item_specification_details(self): def get_item_specification_details(self):
if not self.quality_inspection_template: if not self.quality_inspection_template:
@ -29,9 +29,7 @@ class QualityInspection(Document):
parameters = get_template_details(self.quality_inspection_template) parameters = get_template_details(self.quality_inspection_template)
for d in parameters: for d in parameters:
child = self.append('readings', {}) child = self.append('readings', {})
child.specification = d.specification child.update(d)
child.value = d.value
child.acceptance_formula = d.acceptance_formula
child.status = "Accepted" child.status = "Accepted"
def get_quality_inspection_template(self): def get_quality_inspection_template(self):
@ -69,35 +67,98 @@ class QualityInspection(Document):
doctype = 'Stock Entry Detail' doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name: if self.reference_type and self.reference_name:
conditions = ""
if self.batch_no and self.docstatus == 1:
conditions += " and t1.batch_no = '%s'"%(self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
conditions += " and t1.quality_inspection = '%s'"%(self.name)
frappe.db.sql(""" frappe.db.sql("""
UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 UPDATE
SET t1.quality_inspection = %s, t2.modified = %s `tab{child_doc}` t1, `tab{parent_doc}` t2
WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name SET
""".format(parent_doc=self.reference_type, child_doc=doctype), t1.quality_inspection = %s, t2.modified = %s
WHERE
t1.parent = %s
and t1.item_code = %s
and t1.parent = t2.name
{conditions}
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(quality_inspection, self.modified, self.reference_name, self.item_code)) (quality_inspection, self.modified, self.reference_name, self.item_code))
def set_status_based_on_acceptance_formula(self): def inspect_and_set_status(self):
for reading in self.readings: for reading in self.readings:
if not reading.acceptance_formula: continue if not reading.manual_inspection: # dont auto set status if manual
if reading.formula_based_criteria:
self.set_status_based_on_acceptance_formula(reading)
else:
# if not formula based check acceptance values set
self.set_status_based_on_acceptance_values(reading)
condition = reading.acceptance_formula def set_status_based_on_acceptance_values(self, reading):
data = {} if cint(reading.non_numeric):
result = reading.get("reading_value") == reading.get("value")
else:
# numeric readings
result = self.min_max_criteria_passed(reading)
reading.status = "Accepted" if result else "Rejected"
def min_max_criteria_passed(self, reading):
"""Determine whether all readings fall in the acceptable range."""
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
if not result: return False
return True
def set_status_based_on_acceptance_formula(self, reading):
if not reading.acceptance_formula:
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
title=_("Missing Formula"))
condition = reading.acceptance_formula
data = self.get_formula_evaluation_data(reading)
try:
result = frappe.safe_eval(condition, None, data)
reading.status = "Accepted" if result else "Rejected"
except NameError as e:
field = frappe.bold(e.args[0].split()[1])
frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
.format(reading.idx, field),
title=_("Invalid Formula"))
except Exception:
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
title=_("Invalid Formula"))
def get_formula_evaluation_data(self, reading):
data = {}
if cint(reading.non_numeric):
data = {"reading_value": reading.get("reading_value")}
else:
# numeric readings
for i in range(1, 11): for i in range(1, 11):
field = "reading_" + str(i) field = "reading_" + str(i)
data[field] = flt(reading.get(field)) or 0 data[field] = flt(reading.get(field))
data["mean"] = self.calculate_mean(reading)
try: return data
result = frappe.safe_eval(condition, None, data)
reading.status = "Accepted" if result else "Rejected"
except SyntaxError:
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
title=_("Invalid Formula"))
except NameError as e:
field = frappe.bold(e.args[0].split()[1])
frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
.format(reading.idx, field),
title=_("Invalid Formula"))
def calculate_mean(self, reading):
"""Calculate mean of all non-empty readings."""
from statistics import mean
readings_list = []
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
readings_list.append(flt(reading_value))
actual_mean = mean(readings_list) if readings_list else 0
return actual_mean
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
qa.delete() qa.delete()
dn.delete() dn.delete()
def test_value_based_qi_readings(self):
# Test QI based on acceptance values (Non formula)
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{
"specification": "Iron Content", # numeric reading
"min_value": 0.1,
"max_value": 0.9,
"reading_1": "0.4"
},
{
"specification": "Particle Inspection Needed", # non-numeric reading
"non_numeric": 1,
"value": "Yes",
"reading_value": "Yes"
}]
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
readings=readings, do_not_save=True)
qa.save()
# status must be auto set as per formula
self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Accepted")
qa.delete()
dn.delete()
def test_formula_based_qi_readings(self): def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{ readings = [{
"specification": "Iron Content", "specification": "Iron Content", # numeric reading
"formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
"reading_1": 0.4 "reading_1": "0.4"
}, },
{ {
"specification": "Calcium Content", "specification": "Calcium Content", # numeric reading
"formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
"reading_1": 0.7 "reading_1": "0.7"
}, },
{ {
"specification": "Mg Content", "specification": "Mg Content", # numeric reading
"acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", "formula_based_criteria": 1,
"reading_1": 0.5, "acceptance_formula": "mean < 0.9",
"reading_2": 0.7, "reading_1": "0.5",
"reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues "reading_3": "random text" # check if random string input causes issues
},
{
"specification": "Calcium Content", # non-numeric reading
"formula_based_criteria": 1,
"non_numeric": 1,
"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
"reading_value": "Grade B"
}] }]
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase):
self.assertEqual(qa.readings[0].status, "Accepted") self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected") self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted") self.assertEqual(qa.readings[2].status, "Accepted")
self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete() qa.delete()
dn.delete() dn.delete()
@ -86,11 +124,20 @@ def create_quality_inspection(**args):
qa.item_code = args.item_code or "_Test Item with QA" qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1 qa.sample_size = 1
qa.inspected_by = frappe.session.user qa.inspected_by = frappe.session.user
qa.status = args.status or "Accepted"
readings = args.readings or {"specification": "Size", "status": args.status} if not args.readings:
create_quality_inspection_parameter("Size")
readings = {"specification": "Size", "min_value": 0, "max_value": 10}
else:
readings = args.readings
if args.status == "Rejected":
readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list): if isinstance(readings, list):
for entry in readings: for entry in readings:
create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry) qa.append("readings", entry)
else: else:
qa.append("readings", readings) qa.append("readings", readings)
@ -101,3 +148,11 @@ def create_quality_inspection(**args):
qa.submit() qa.submit()
return qa return qa
def create_quality_inspection_parameter(parameter):
if not frappe.db.exists("Quality Inspection Parameter", parameter):
frappe.get_doc({
"doctype": "Quality Inspection Parameter",
"parameter": parameter,
"description": parameter
}).insert()

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