Merge branch 'version-13-hotfix' into qi-rejection
This commit is contained in:
commit
01be96adb2
@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
|
|||||||
if address and frappe.db.get_value('Dynamic Link',
|
if address and frappe.db.get_value('Dynamic Link',
|
||||||
{'parent': address, 'link_name': company}):
|
{'parent': address, 'link_name': company}):
|
||||||
filters.append(["Address", "name", "=", address])
|
filters.append(["Address", "name", "=", address])
|
||||||
|
if not address:
|
||||||
|
filters.append(["Address", "is_shipping_address", "=", 1])
|
||||||
|
|
||||||
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||||
|
|
||||||
|
@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
|
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
|
||||||
total_days, total_booking_days, account_currency)
|
total_days, total_booking_days, account_currency)
|
||||||
|
|
||||||
|
if not amount:
|
||||||
|
return
|
||||||
|
|
||||||
if via_journal_entry:
|
if via_journal_entry:
|
||||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||||
@ -298,9 +301,13 @@ def process_deferred_accounting(posting_date=None):
|
|||||||
start_date = add_months(today(), -1)
|
start_date = add_months(today(), -1)
|
||||||
end_date = add_days(today(), -1)
|
end_date = add_days(today(), -1)
|
||||||
|
|
||||||
|
companies = frappe.get_all('Company')
|
||||||
|
|
||||||
|
for company in companies:
|
||||||
for record_type in ('Income', 'Expense'):
|
for record_type in ('Income', 'Expense'):
|
||||||
doc = frappe.get_doc(dict(
|
doc = frappe.get_doc(dict(
|
||||||
doctype='Process Deferred Accounting',
|
doctype='Process Deferred Accounting',
|
||||||
|
company=company.name,
|
||||||
posting_date=posting_date,
|
posting_date=posting_date,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
|
@ -19,7 +19,7 @@ class AccountingDimension(Document):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
|
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
|
||||||
'Cost Center', 'Accounting Dimension Detail', 'Company') :
|
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
|
||||||
|
|
||||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||||
frappe.throw(msg)
|
frappe.throw(msg)
|
||||||
|
@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
|
|||||||
self.import_file, self.google_sheets_url
|
self.import_file, self.google_sheets_url
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'Bank Account' not in json.dumps(preview):
|
if 'Bank Account' not in json.dumps(preview['columns']):
|
||||||
frappe.throw(_("Please add the Bank Account column"))
|
frappe.throw(_("Please add the Bank Account column"))
|
||||||
|
|
||||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||||
|
@ -690,7 +690,7 @@
|
|||||||
"options": "Account"
|
"options": "Account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.received_amount",
|
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||||
"fieldname": "received_amount_after_tax",
|
"fieldname": "received_amount_after_tax",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Received Amount After Tax",
|
"label": "Received Amount After Tax",
|
||||||
@ -707,7 +707,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-09 11:55:04.215050",
|
"modified": "2021-06-22 20:37:06.154206",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
@ -706,7 +706,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if account_currency != self.company_currency:
|
if account_currency != self.company_currency:
|
||||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
|
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
|
||||||
|
|
||||||
if self.payment_type == 'Pay':
|
if self.payment_type in ('Pay', 'Internal Transfer'):
|
||||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||||
elif self.payment_type == 'Receive':
|
elif self.payment_type == 'Receive':
|
||||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||||
@ -761,7 +761,7 @@ class PaymentEntry(AccountsController):
|
|||||||
return self.advance_tax_account
|
return self.advance_tax_account
|
||||||
elif self.payment_type == 'Receive':
|
elif self.payment_type == 'Receive':
|
||||||
return self.paid_from
|
return self.paid_from
|
||||||
elif self.payment_type == 'Pay':
|
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||||
return self.paid_to
|
return self.paid_to
|
||||||
|
|
||||||
def update_advance_paid(self):
|
def update_advance_paid(self):
|
||||||
|
@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
|
|||||||
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
|
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
|
||||||
|
|
||||||
self.assertEqual(pe.cost_center, si.cost_center)
|
self.assertEqual(pe.cost_center, si.cost_center)
|
||||||
self.assertEqual(expected_account_balance, account_balance)
|
self.assertEqual(flt(expected_account_balance), account_balance)
|
||||||
self.assertEqual(expected_party_balance, party_balance)
|
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||||
self.assertEqual(expected_party_account_balance, party_account_balance)
|
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||||
|
|
||||||
def create_payment_terms_template():
|
def create_payment_terms_template():
|
||||||
|
|
||||||
|
@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
||||||
billing_email = frappe.db.sql("""
|
billing_email = frappe.db.sql("""
|
||||||
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
|
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
|
||||||
WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
|
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
|
||||||
c.is_billing_contact=1 \
|
order by c.creation desc""", customer_name)
|
||||||
order by c.creation desc""")
|
|
||||||
|
|
||||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||||
if billing_and_primary:
|
if billing_and_primary:
|
||||||
|
@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
company: function() {
|
|
||||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
|
||||||
},
|
|
||||||
|
|
||||||
onload: function() {
|
onload: function() {
|
||||||
this._super();
|
this._super();
|
||||||
|
|
||||||
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
frm: frm,
|
frm: frm,
|
||||||
freeze_message: __("Creating Purchase Receipt ...")
|
freeze_message: __("Creating Purchase Receipt ...")
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
|
||||||
|
company: function(frm) {
|
||||||
|
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
||||||
|
|
||||||
# Create Purchase Order with TDS applied
|
# Create Purchase Order with TDS applied
|
||||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000)
|
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
|
||||||
po.apply_tds = 1
|
po.apply_tds = 1
|
||||||
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||||
po.save()
|
po.save()
|
||||||
@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
# Create Purchase Invoice against Purchase Order
|
# Create Purchase Invoice against Purchase Order
|
||||||
purchase_invoice = get_mapped_purchase_invoice(po.name)
|
purchase_invoice = get_mapped_purchase_invoice(po.name)
|
||||||
purchase_invoice.allocate_advances_automatically = 1
|
purchase_invoice.allocate_advances_automatically = 1
|
||||||
|
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
|
||||||
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
|
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
|
||||||
purchase_invoice.save()
|
purchase_invoice.save()
|
||||||
purchase_invoice.submit()
|
purchase_invoice.submit()
|
||||||
@ -1009,21 +1010,21 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
# Check GLE for Purchase Invoice
|
# Check GLE for Purchase Invoice
|
||||||
# Zero net effect on final TDS Payable on invoice
|
# Zero net effect on final TDS Payable on invoice
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
|
['_Test Account Cost for Goods Sold - _TC', 30000],
|
||||||
['_Test Account Excise Duty - _TC', 0, 3000],
|
['_Test Account Excise Duty - _TC', -3000],
|
||||||
['Creditors - _TC', 0, 27000],
|
['Creditors - _TC', -27000],
|
||||||
['TDS Payable - _TC', 3000, 3000]
|
['TDS Payable - _TC', 0]
|
||||||
]
|
]
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where voucher_type='Purchase Invoice' and voucher_no=%s
|
where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||||
|
group by account
|
||||||
order by account asc""", (purchase_invoice.name), as_dict=1)
|
order by account asc""", (purchase_invoice.name), as_dict=1)
|
||||||
|
|
||||||
for i, gle in enumerate(gl_entries):
|
for i, gle in enumerate(gl_entries):
|
||||||
self.assertEqual(expected_gle[i][0], gle.account)
|
self.assertEqual(expected_gle[i][0], gle.account)
|
||||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
|
||||||
|
|
||||||
def update_tax_witholding_category(company, account, date):
|
def update_tax_witholding_category(company, account, date):
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
einvoice = make_einvoice(si)
|
einvoice = make_einvoice(si)
|
||||||
validate_totals(einvoice)
|
validate_totals(einvoice)
|
||||||
|
|
||||||
|
def test_item_tax_net_range(self):
|
||||||
|
item = create_item("T Shirt")
|
||||||
|
|
||||||
|
item.set('taxes', [])
|
||||||
|
item.append("taxes", {
|
||||||
|
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||||
|
"minimum_net_rate": 0,
|
||||||
|
"maximum_net_rate": 500
|
||||||
|
})
|
||||||
|
|
||||||
|
item.append("taxes", {
|
||||||
|
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||||
|
"minimum_net_rate": 501,
|
||||||
|
"maximum_net_rate": 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
|
||||||
|
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||||
|
|
||||||
|
# Apply discount
|
||||||
|
sales_invoice.apply_discount_on = 'Net Total'
|
||||||
|
sales_invoice.discount_amount = 300
|
||||||
|
sales_invoice.save()
|
||||||
|
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||||
|
|
||||||
def get_sales_invoice_for_e_invoice():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
@ -1985,32 +2012,6 @@ def get_sales_invoice_for_e_invoice():
|
|||||||
|
|
||||||
return si
|
return si
|
||||||
|
|
||||||
def test_item_tax_net_range(self):
|
|
||||||
item = create_item("T Shirt")
|
|
||||||
|
|
||||||
item.set('taxes', [])
|
|
||||||
item.append("taxes", {
|
|
||||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
|
||||||
"minimum_net_rate": 0,
|
|
||||||
"maximum_net_rate": 500
|
|
||||||
})
|
|
||||||
|
|
||||||
item.append("taxes", {
|
|
||||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
|
||||||
"minimum_net_rate": 501,
|
|
||||||
"maximum_net_rate": 1000
|
|
||||||
})
|
|
||||||
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
|
|
||||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
|
||||||
|
|
||||||
# Apply discount
|
|
||||||
sales_invoice.apply_discount_on = 'Net Total'
|
|
||||||
sales_invoice.discount_amount = 300
|
|
||||||
sales_invoice.save()
|
|
||||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
|
||||||
|
|
||||||
def make_test_address_for_ewaybill():
|
def make_test_address_for_ewaybill():
|
||||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
||||||
@ -2087,9 +2088,9 @@ def make_sales_invoice_for_ewaybill():
|
|||||||
if not gst_account:
|
if not gst_account:
|
||||||
gst_settings.append("gst_accounts", {
|
gst_settings.append("gst_accounts", {
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"cgst_account": "CGST - _TC",
|
"cgst_account": "Output Tax CGST - _TC",
|
||||||
"sgst_account": "SGST - _TC",
|
"sgst_account": "Output Tax SGST - _TC",
|
||||||
"igst_account": "IGST - _TC",
|
"igst_account": "Output Tax IGST - _TC",
|
||||||
})
|
})
|
||||||
|
|
||||||
gst_settings.save()
|
gst_settings.save()
|
||||||
@ -2106,7 +2107,7 @@ def make_sales_invoice_for_ewaybill():
|
|||||||
|
|
||||||
si.append("taxes", {
|
si.append("taxes", {
|
||||||
"charge_type": "On Net Total",
|
"charge_type": "On Net Total",
|
||||||
"account_head": "CGST - _TC",
|
"account_head": "Output Tax CGST - _TC",
|
||||||
"cost_center": "Main - _TC",
|
"cost_center": "Main - _TC",
|
||||||
"description": "CGST @ 9.0",
|
"description": "CGST @ 9.0",
|
||||||
"rate": 9
|
"rate": 9
|
||||||
@ -2114,7 +2115,7 @@ def make_sales_invoice_for_ewaybill():
|
|||||||
|
|
||||||
si.append("taxes", {
|
si.append("taxes", {
|
||||||
"charge_type": "On Net Total",
|
"charge_type": "On Net Total",
|
||||||
"account_head": "SGST - _TC",
|
"account_head": "Output Tax SGST - _TC",
|
||||||
"cost_center": "Main - _TC",
|
"cost_center": "Main - _TC",
|
||||||
"description": "SGST @ 9.0",
|
"description": "SGST @ 9.0",
|
||||||
"rate": 9
|
"rate": 9
|
||||||
|
@ -1,24 +1,6 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
|
|
||||||
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
|
|
||||||
|
|
||||||
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
|
|
||||||
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
|
|
||||||
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
|
|
||||||
})
|
|
||||||
|
|
||||||
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
|
|
||||||
if(frm.doc.__islocal) {
|
|
||||||
frm.set_value("use_for_shopping_cart", 1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
|
|
||||||
frappe.ui.form.trigger("Tax Rule", "tax_type");
|
|
||||||
})
|
|
||||||
|
|
||||||
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
|
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
|
||||||
if(frm.doc.customer) {
|
if(frm.doc.customer) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
|
|||||||
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
|
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
|
||||||
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
|
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
|
||||||
tax_rule1.save()
|
tax_rule1.save()
|
||||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
|
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
|
||||||
"_Test Sales Taxes and Charges Template - _TC")
|
"_Test Sales Taxes and Charges Template - _TC")
|
||||||
|
|
||||||
def test_conflict_with_overlapping_dates(self):
|
def test_conflict_with_overlapping_dates(self):
|
||||||
|
@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
|
|
||||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||||
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
||||||
'cost_center', 'project']
|
'cost_center', 'project', 'voucher_detail_no']
|
||||||
|
|
||||||
if dimensions:
|
if dimensions:
|
||||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||||
|
@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
|||||||
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where party_type = %s and party=%s
|
where party_type = %s and party=%s
|
||||||
|
and is_cancelled = 0
|
||||||
group by company""", (party_type, party)))
|
group by company""", (party_type, party)))
|
||||||
|
|
||||||
for d in companies:
|
for d in companies:
|
||||||
|
@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
|
|||||||
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
||||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||||
acc.account_name, acc.account_number
|
acc.account_name, acc.account_number
|
||||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
|
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||||
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
|
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
|
||||||
{
|
{
|
||||||
|
@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
if filters.get("account") and not filters.get("include_dimensions"):
|
if filters.get("account"):
|
||||||
filters.account = get_accounts_with_children(filters.account)
|
filters.account = get_accounts_with_children(filters.account)
|
||||||
conditions.append("account in %(account)s")
|
conditions.append("account in %(account)s")
|
||||||
|
|
||||||
|
@ -168,21 +168,24 @@ def get_columns(filters):
|
|||||||
"label": _("Income"),
|
"label": _("Income"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 120
|
"width": 305
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "expense",
|
"fieldname": "expense",
|
||||||
"label": _("Expense"),
|
"label": _("Expense"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 120
|
"width": 305
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "gross_profit_loss",
|
"fieldname": "gross_profit_loss",
|
||||||
"label": _("Gross Profit / Loss"),
|
"label": _("Gross Profit / Loss"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 120
|
"width": 307
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -784,7 +784,7 @@ def get_children(doctype, parent, company, is_root=False):
|
|||||||
return acc
|
return acc
|
||||||
|
|
||||||
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||||
|
|
||||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||||
if not company:
|
if not company:
|
||||||
|
@ -9,13 +9,14 @@
|
|||||||
"supp_master_name",
|
"supp_master_name",
|
||||||
"supplier_group",
|
"supplier_group",
|
||||||
"buying_price_list",
|
"buying_price_list",
|
||||||
|
"maintain_same_rate_action",
|
||||||
|
"role_to_override_stop_action",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"po_required",
|
"po_required",
|
||||||
"pr_required",
|
"pr_required",
|
||||||
"maintain_same_rate",
|
"maintain_same_rate",
|
||||||
"maintain_same_rate_action",
|
|
||||||
"role_to_override_stop_action",
|
|
||||||
"allow_multiple_items",
|
"allow_multiple_items",
|
||||||
|
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||||
"subcontract",
|
"subcontract",
|
||||||
"backflush_raw_materials_of_subcontract_based_on",
|
"backflush_raw_materials_of_subcontract_based_on",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
@ -108,6 +109,13 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Override Stop Action",
|
"label": "Role Allowed to Override Stop Action",
|
||||||
"options": "Role"
|
"options": "Role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||||
|
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@ -115,7 +123,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-04 20:01:44.087066",
|
"modified": "2021-06-24 10:38:28.934525",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
@ -97,6 +97,9 @@
|
|||||||
"is_fixed_asset",
|
"is_fixed_asset",
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"section_break_72",
|
"section_break_72",
|
||||||
|
"production_plan",
|
||||||
|
"production_plan_item",
|
||||||
|
"production_plan_sub_assembly_item",
|
||||||
"page_break"
|
"page_break"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -803,13 +806,37 @@
|
|||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_plan",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Production Plan",
|
||||||
|
"options": "Production Plan",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_plan_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Production Plan Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_plan_sub_assembly_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Production Plan Sub Assembly Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-22 11:46:12.357435",
|
"modified": "2021-06-28 19:22:22.715365",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
@ -828,6 +828,12 @@ class AccountsController(TransactionBase):
|
|||||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||||
|
|
||||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||||
|
if self.doctype != "Purchase Invoice":
|
||||||
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
|
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||||
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
|
|
||||||
|
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||||
.format(item.item_code, item.idx, max_allowed_amt))
|
.format(item.item_code, item.idx, max_allowed_amt))
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
fields = get_fields("Employee", ["name", "employee_name"])
|
fields = get_fields("Employee", ["name", "employee_name"])
|
||||||
|
|
||||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||||
where status = 'Active'
|
where status in ('Active', 'Suspended')
|
||||||
and docstatus < 2
|
and docstatus < 2
|
||||||
and ({key} like %(txt)s
|
and ({key} like %(txt)s
|
||||||
or employee_name like %(txt)s)
|
or employee_name like %(txt)s)
|
||||||
@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
return frappe.db.sql("""select {fields} from `tabProject`
|
return frappe.db.sql("""select {fields} from `tabProject`
|
||||||
where
|
where
|
||||||
`tabProject`.status not in ("Completed", "Cancelled")
|
`tabProject`.status not in ("Completed", "Cancelled")
|
||||||
and {cond} {match_cond} {scond}
|
and {cond} {scond} {match_cond}
|
||||||
order by
|
order by
|
||||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
|
@ -99,8 +99,9 @@ def validate_returned_items(doc):
|
|||||||
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
|
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
|
||||||
.format(d.idx, s, doc.doctype, doc.return_against))
|
.format(d.idx, s, doc.doctype, doc.return_against))
|
||||||
|
|
||||||
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \
|
if (warehouse_mandatory and not d.get("warehouse") and
|
||||||
and not d.get("warehouse"):
|
frappe.db.get_value("Item", d.item_code, "is_stock_item")
|
||||||
|
):
|
||||||
frappe.throw(_("Warehouse is mandatory"))
|
frappe.throw(_("Warehouse is mandatory"))
|
||||||
|
|
||||||
items_returned = True
|
items_returned = True
|
||||||
|
@ -330,9 +330,15 @@ class SellingController(StockController):
|
|||||||
|
|
||||||
# For internal transfers use incoming rate as the valuation rate
|
# For internal transfers use incoming rate as the valuation rate
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
|
if d.doctype == "Packed Item":
|
||||||
|
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
|
||||||
|
if d.incoming_rate != incoming_rate:
|
||||||
|
d.incoming_rate = incoming_rate
|
||||||
|
else:
|
||||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
|
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
|
||||||
if d.rate != rate:
|
if d.rate != rate:
|
||||||
d.rate = rate
|
d.rate = rate
|
||||||
|
|
||||||
d.discount_percentage = 0
|
d.discount_percentage = 0
|
||||||
d.discount_amount = 0
|
d.discount_amount = 0
|
||||||
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
|
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
|
||||||
|
@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||||
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
@ -523,9 +523,6 @@ class StockController(AccountsController):
|
|||||||
})
|
})
|
||||||
if future_sle_exists(args):
|
if future_sle_exists(args):
|
||||||
create_repost_item_valuation_entry(args)
|
create_repost_item_valuation_entry(args)
|
||||||
elif not is_reposting_pending():
|
|
||||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
|
||||||
self.company, self.doctype, self.name)
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_quality_inspections(doctype, docname, items):
|
def make_quality_inspections(doctype, docname, items):
|
||||||
|
@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
|
|||||||
item.margin_type = None
|
item.margin_type = None
|
||||||
item.margin_rate_or_amount = 0.0
|
item.margin_rate_or_amount = 0.0
|
||||||
|
|
||||||
if item.margin_type and item.margin_rate_or_amount:
|
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
|
||||||
|
item.margin_type = "Amount"
|
||||||
|
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
|
||||||
|
item.precision("margin_rate_or_amount"))
|
||||||
|
item.rate_with_margin = item.rate
|
||||||
|
|
||||||
|
elif item.margin_type and item.margin_rate_or_amount:
|
||||||
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
|
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
|
||||||
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
||||||
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
||||||
|
@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-01-28 16:16:45.447213",
|
"modified": "2021-06-29 18:27:02.832979",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Appointment",
|
"name": "Appointment",
|
||||||
@ -153,6 +153,18 @@
|
|||||||
"role": "Sales User",
|
"role": "Sales User",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Employee",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
@ -168,12 +168,13 @@ class Lead(SellingController):
|
|||||||
if self.phone:
|
if self.phone:
|
||||||
contact.append("phone_nos", {
|
contact.append("phone_nos", {
|
||||||
"phone": self.phone,
|
"phone": self.phone,
|
||||||
"is_primary": 1
|
"is_primary_phone": 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.mobile_no:
|
if self.mobile_no:
|
||||||
contact.append("phone_nos", {
|
contact.append("phone_nos", {
|
||||||
"phone": self.mobile_no
|
"phone": self.mobile_no,
|
||||||
|
"is_primary_mobile_no":1
|
||||||
})
|
})
|
||||||
|
|
||||||
contact.insert(ignore_permissions=True)
|
contact.insert(ignore_permissions=True)
|
||||||
|
@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
|||||||
get_chart_data: function (_columns, result) {
|
get_chart_data: function (_columns, result) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
labels: result.map(d => d[0]),
|
labels: result.map(d => d.creation_date),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
name: "First Response Time",
|
name: "First Response Time",
|
||||||
values: result.map(d => d[1])
|
values: result.map(d => d.first_response_time)
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
type: "line",
|
type: "line",
|
||||||
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
|||||||
hide_days: 0,
|
hide_days: 0,
|
||||||
hide_seconds: 0
|
hide_seconds: 0
|
||||||
};
|
};
|
||||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
|
|||||||
student = get_current_student()
|
student = get_current_student()
|
||||||
course_enrollment = get_enrollment("course", course, student.name)
|
course_enrollment = get_enrollment("course", course, student.name)
|
||||||
if not course_enrollment:
|
if not course_enrollment:
|
||||||
program_enrollment = get_enrollment('program', program, student.name)
|
program_enrollment = get_enrollment('program', program.name, student.name)
|
||||||
if not program_enrollment:
|
if not program_enrollment:
|
||||||
frappe.throw(_("You are not enrolled in program {0}").format(program))
|
frappe.throw(_("You are not enrolled in program {0}").format(program))
|
||||||
return
|
return
|
||||||
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
|
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
|
||||||
else:
|
else:
|
||||||
return frappe.get_doc('Course Enrollment', course_enrollment)
|
return frappe.get_doc('Course Enrollment', course_enrollment)
|
||||||
|
|
||||||
|
@ -7,16 +7,21 @@ import frappe
|
|||||||
import unittest
|
import unittest
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
|
||||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||||
|
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
||||||
|
|
||||||
class TestMpesaSettings(unittest.TestCase):
|
class TestMpesaSettings(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# create payment gateway in setup
|
||||||
|
create_mpesa_settings(payment_gateway_name="_Test")
|
||||||
|
create_mpesa_settings(payment_gateway_name="_Account Balance")
|
||||||
|
create_mpesa_settings(payment_gateway_name="Payment")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.sql('delete from `tabMpesa Settings`')
|
frappe.db.sql('delete from `tabMpesa Settings`')
|
||||||
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
|
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
|
||||||
|
|
||||||
def test_creation_of_payment_gateway(self):
|
def test_creation_of_payment_gateway(self):
|
||||||
create_mpesa_settings(payment_gateway_name="_Test")
|
mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
|
||||||
|
|
||||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
|
|
||||||
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
||||||
self.assertTrue(mode_of_payment.name)
|
self.assertTrue(mode_of_payment.name)
|
||||||
self.assertEqual(mode_of_payment.type, "Phone")
|
self.assertEqual(mode_of_payment.type, "Phone")
|
||||||
@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
|
|||||||
integration_request.delete()
|
integration_request.delete()
|
||||||
|
|
||||||
def test_processing_of_callback_payload(self):
|
def test_processing_of_callback_payload(self):
|
||||||
create_mpesa_settings(payment_gateway_name="Payment")
|
|
||||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
||||||
@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
|
|||||||
pos_invoice.delete()
|
pos_invoice.delete()
|
||||||
|
|
||||||
def test_processing_of_multiple_callback_payload(self):
|
def test_processing_of_multiple_callback_payload(self):
|
||||||
create_mpesa_settings(payment_gateway_name="Payment")
|
|
||||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
||||||
@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
|
|||||||
pos_invoice.delete()
|
pos_invoice.delete()
|
||||||
|
|
||||||
def test_processing_of_only_one_succes_callback_payload(self):
|
def test_processing_of_only_one_succes_callback_payload(self):
|
||||||
create_mpesa_settings(payment_gateway_name="Payment")
|
|
||||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
||||||
@ -202,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"):
|
|||||||
|
|
||||||
doc = frappe.get_doc(dict( #nosec
|
doc = frappe.get_doc(dict( #nosec
|
||||||
doctype="Mpesa Settings",
|
doctype="Mpesa Settings",
|
||||||
|
sandbox=1,
|
||||||
payment_gateway_name=payment_gateway_name,
|
payment_gateway_name=payment_gateway_name,
|
||||||
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
||||||
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
||||||
|
@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
|||||||
"payment_gateway": gateway
|
"payment_gateway": gateway
|
||||||
}, ['payment_account'])
|
}, ['payment_account'])
|
||||||
|
|
||||||
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
|
mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
|
||||||
|
if not mode_of_payment and payment_gateway_account:
|
||||||
mode_of_payment = frappe.get_doc({
|
mode_of_payment = frappe.get_doc({
|
||||||
"doctype": "Mode of Payment",
|
"doctype": "Mode of Payment",
|
||||||
"mode_of_payment": gateway,
|
"mode_of_payment": gateway,
|
||||||
@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
|||||||
})
|
})
|
||||||
mode_of_payment.insert(ignore_permissions=True)
|
mode_of_payment.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
return mode_of_payment
|
||||||
|
elif mode_of_payment:
|
||||||
|
return frappe.get_doc("Mode of Payment", mode_of_payment)
|
||||||
|
|
||||||
def get_tracking_url(carrier, tracking_number):
|
def get_tracking_url(carrier, tracking_number):
|
||||||
# Return the formatted Tracking URL.
|
# Return the formatted Tracking URL.
|
||||||
tracking_url = ''
|
tracking_url = ''
|
||||||
|
@ -157,6 +157,7 @@ website_route_rules = [
|
|||||||
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
|
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{"from_route": "/project", "to_route": "Project"}
|
||||||
]
|
]
|
||||||
|
|
||||||
standard_portal_menu_items = [
|
standard_portal_menu_items = [
|
||||||
|
@ -15,6 +15,7 @@ class Attendance(Document):
|
|||||||
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
||||||
self.validate_attendance_date()
|
self.validate_attendance_date()
|
||||||
self.validate_duplicate_record()
|
self.validate_duplicate_record()
|
||||||
|
self.validate_employee_status()
|
||||||
self.check_leave_record()
|
self.check_leave_record()
|
||||||
|
|
||||||
def validate_attendance_date(self):
|
def validate_attendance_date(self):
|
||||||
@ -38,6 +39,10 @@ class Attendance(Document):
|
|||||||
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
|
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
|
||||||
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
|
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
|
||||||
|
|
||||||
|
def validate_employee_status(self):
|
||||||
|
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
|
||||||
|
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
|
||||||
|
|
||||||
def check_leave_record(self):
|
def check_leave_record(self):
|
||||||
leave_record = frappe.db.sql("""
|
leave_record = frappe.db.sql("""
|
||||||
select leave_type, half_day, half_day_date
|
select leave_type, half_day, half_day_date
|
||||||
|
@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
label: __('For Employee'),
|
label: __('For Employee'),
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
options: 'Employee',
|
options: 'Employee',
|
||||||
|
get_query: () => {
|
||||||
|
return {query: "erpnext.controllers.queries.employee_query"}
|
||||||
|
},
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
onchange: function() {
|
onchange: function() {
|
||||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||||
|
@ -207,7 +207,7 @@
|
|||||||
"label": "Status",
|
"label": "Status",
|
||||||
"oldfieldname": "status",
|
"oldfieldname": "status",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "Active\nInactive\nLeft",
|
"options": "Active\nInactive\nSuspended\nLeft",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
@ -813,7 +813,7 @@
|
|||||||
"idx": 24,
|
"idx": 24,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-12 11:31:37.730760",
|
"modified": "2021-06-17 11:31:37.730760",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
|
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
|
||||||
from frappe.model.naming import set_name_by_naming_series
|
from frappe.model.naming import set_name_by_naming_series
|
||||||
from frappe import throw, _, scrub
|
from frappe import throw, _, scrub
|
||||||
from frappe.permissions import add_user_permission, remove_user_permission, \
|
from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||||
@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.utilities.transaction_base import delete_events
|
from erpnext.utilities.transaction_base import delete_events
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet
|
||||||
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
|
|
||||||
|
|
||||||
class EmployeeUserDisabledError(frappe.ValidationError): pass
|
class EmployeeUserDisabledError(frappe.ValidationError): pass
|
||||||
class EmployeeLeftValidationError(frappe.ValidationError): pass
|
class EmployeeLeftValidationError(frappe.ValidationError): pass
|
||||||
@ -37,7 +36,7 @@ class Employee(NestedSet):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
from erpnext.controllers.status_updater import validate_status
|
from erpnext.controllers.status_updater import validate_status
|
||||||
validate_status(self.status, ["Active", "Inactive", "Left"])
|
validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
|
||||||
|
|
||||||
self.employee = self.name
|
self.employee = self.name
|
||||||
self.set_employee_name()
|
self.set_employee_name()
|
||||||
|
@ -7,7 +7,8 @@ def get_data():
|
|||||||
'heatmap_message': _('This is based on the attendance of this Employee'),
|
'heatmap_message': _('This is based on the attendance of this Employee'),
|
||||||
'fieldname': 'employee',
|
'fieldname': 'employee',
|
||||||
'non_standard_fieldnames': {
|
'non_standard_fieldnames': {
|
||||||
'Bank Account': 'party'
|
'Bank Account': 'party',
|
||||||
|
'Employee Grievance': 'raised_by'
|
||||||
},
|
},
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
@ -20,7 +21,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Lifecycle'),
|
'label': _('Lifecycle'),
|
||||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
|
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Shift'),
|
'label': _('Shift'),
|
||||||
|
@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
|
|||||||
filters: [["status","=", "Active"]],
|
filters: [["status","=", "Active"]],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||||
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
|
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
|
||||||
return indicator;
|
return indicator;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Employee Grievance', {
|
||||||
|
setup: function(frm) {
|
||||||
|
frm.set_query('grievance_against_party', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ['in', [
|
||||||
|
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
frm.set_query('associated_document_type', function() {
|
||||||
|
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
|
||||||
|
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
istable: 0,
|
||||||
|
issingle: 0,
|
||||||
|
module: ["Not In", ignore_modules]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
grievance_against_party: function(frm) {
|
||||||
|
let filters = {};
|
||||||
|
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
|
||||||
|
filters.name = ["!=", frm.doc.raised_by];
|
||||||
|
}
|
||||||
|
frm.set_query('grievance_against', function() {
|
||||||
|
return {
|
||||||
|
filters: filters
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "HR-GRIEV-.YYYY.-.#####",
|
||||||
|
"creation": "2021-05-11 13:41:51.485295",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"subject",
|
||||||
|
"raised_by",
|
||||||
|
"employee_name",
|
||||||
|
"designation",
|
||||||
|
"column_break_3",
|
||||||
|
"date",
|
||||||
|
"status",
|
||||||
|
"reports_to",
|
||||||
|
"grievance_details_section",
|
||||||
|
"grievance_against_party",
|
||||||
|
"grievance_against",
|
||||||
|
"grievance_type",
|
||||||
|
"column_break_11",
|
||||||
|
"associated_document_type",
|
||||||
|
"associated_document",
|
||||||
|
"section_break_14",
|
||||||
|
"description",
|
||||||
|
"investigation_details_section",
|
||||||
|
"cause_of_grievance",
|
||||||
|
"resolution_details_section",
|
||||||
|
"resolved_by",
|
||||||
|
"resolution_date",
|
||||||
|
"employee_responsible",
|
||||||
|
"column_break_16",
|
||||||
|
"resolution_detail",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "grievance_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Grievance Type",
|
||||||
|
"options": "Grievance Type",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date ",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Open",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Open\nInvestigated\nResolved\nInvalid",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"label": "Description",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cause_of_grievance",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"label": "Cause of Grievance",
|
||||||
|
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "resolution_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Resolution Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "resolved_by",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Resolved By",
|
||||||
|
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "employee_responsible",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Employee Responsible ",
|
||||||
|
"options": "Employee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "resolution_detail",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Resolution Details",
|
||||||
|
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_16",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "resolution_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Resolution Date",
|
||||||
|
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grievance_against",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Grievance Against",
|
||||||
|
"options": "grievance_against_party",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "raised_by",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Raised By",
|
||||||
|
"options": "Employee",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Employee Grievance",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "raised_by.designation",
|
||||||
|
"fieldname": "designation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Designation",
|
||||||
|
"options": "Designation",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "raised_by.reports_to",
|
||||||
|
"fieldname": "reports_to",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Reports To",
|
||||||
|
"options": "Employee",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grievance_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Grievance Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_11",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_14",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grievance_against_party",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Grievance Against Party",
|
||||||
|
"options": "DocType",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "associated_document_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Associated Document Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "associated_document",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Associated Document",
|
||||||
|
"options": "associated_document_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "investigation_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Investigation Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "raised_by.employee_name",
|
||||||
|
"fieldname": "employee_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Employee Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "subject",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Subject",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-21 12:51:01.499486",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "HR",
|
||||||
|
"name": "Employee Grievance",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR Manager",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"search_fields": "subject,raised_by,grievance_against_party",
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"title_field": "subject",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _, bold
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class EmployeeGrievance(Document):
|
||||||
|
def on_submit(self):
|
||||||
|
if self.status not in ["Invalid", "Resolved"]:
|
||||||
|
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
|
||||||
|
bold("Invalid"),
|
||||||
|
bold("Resolved"))
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
frappe.listview_settings["Employee Grievance"] = {
|
||||||
|
has_indicator_for_draft: 1,
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
var colors = {
|
||||||
|
"Open": "red",
|
||||||
|
"Investigated": "orange",
|
||||||
|
"Resolved": "green",
|
||||||
|
"Invalid": "grey"
|
||||||
|
};
|
||||||
|
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from frappe.utils import today
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
class TestEmployeeGrievance(unittest.TestCase):
|
||||||
|
def test_create_employee_grievance(self):
|
||||||
|
create_employee_grievance()
|
||||||
|
|
||||||
|
def create_employee_grievance():
|
||||||
|
grievance_type = create_grievance_type()
|
||||||
|
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
|
||||||
|
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
|
||||||
|
|
||||||
|
grievance = frappe.new_doc("Employee Grievance")
|
||||||
|
grievance.subject = "Test Employee Grievance"
|
||||||
|
grievance.raised_by = emp_1
|
||||||
|
grievance.date = today()
|
||||||
|
grievance.grievance_type = grievance_type
|
||||||
|
grievance.grievance_against_party = "Employee"
|
||||||
|
grievance.grievance_against = emp_2
|
||||||
|
grievance.description = "test descrip"
|
||||||
|
|
||||||
|
#set cause
|
||||||
|
grievance.cause_of_grievance = "test cause"
|
||||||
|
|
||||||
|
#resolution details
|
||||||
|
grievance.resolution_date = today()
|
||||||
|
grievance.resolution_detail = "test resolution detail"
|
||||||
|
grievance.resolved_by = "test_emp_grievance_@example.com"
|
||||||
|
grievance.employee_responsible = emp_2
|
||||||
|
grievance.status = "Resolved"
|
||||||
|
|
||||||
|
grievance.save()
|
||||||
|
grievance.submit()
|
||||||
|
|
||||||
|
return grievance
|
||||||
|
|
||||||
|
|
||||||
|
def create_grievance_type():
|
||||||
|
if frappe.db.exists("Grievance Type", "Employee Abuse"):
|
||||||
|
return frappe.get_doc("Grievance Type", "Employee Abuse")
|
||||||
|
grievance_type = frappe.new_doc("Grievance Type")
|
||||||
|
grievance_type.name = "Employee Abuse"
|
||||||
|
grievance_type.description = "Test"
|
||||||
|
grievance_type.save()
|
||||||
|
|
||||||
|
return grievance_type.name
|
||||||
|
|
@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
def test_expense_claim_gl_entry(self):
|
def test_expense_claim_gl_entry(self):
|
||||||
payable_account = get_payable_account(company_name)
|
payable_account = get_payable_account(company_name)
|
||||||
taxes = generate_taxes()
|
taxes = generate_taxes()
|
||||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes)
|
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
|
||||||
|
do_not_submit=True, taxes=taxes)
|
||||||
expense_claim.submit()
|
expense_claim.submit()
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||||
@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = dict((d[0], d) for d in [
|
expected_values = dict((d[0], d) for d in [
|
||||||
['CGST - _TC4',18.0, 0.0],
|
['Output Tax CGST - _TC4',18.0, 0.0],
|
||||||
[payable_account, 0.0, 218.0],
|
[payable_account, 0.0, 218.0],
|
||||||
["Travel Expenses - _TC4", 200.0, 0.0]
|
["Travel Expenses - _TC4", 200.0, 0.0]
|
||||||
])
|
])
|
||||||
@ -145,7 +146,7 @@ def generate_taxes():
|
|||||||
parent_account = frappe.db.get_value('Account',
|
parent_account = frappe.db.get_value('Account',
|
||||||
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
|
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
|
||||||
'name')
|
'name')
|
||||||
account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account)
|
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
|
||||||
return {'taxes':[{
|
return {'taxes':[{
|
||||||
"account_head": account,
|
"account_head": account,
|
||||||
"rate": 0,
|
"rate": 0,
|
||||||
|
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Grievance Type', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "Prompt",
|
||||||
|
"creation": "2021-05-11 12:41:50.256071",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"section_break_5",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"label": "Description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-21 12:54:37.764712",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "HR",
|
||||||
|
"name": "Grievance Type",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class GrievanceType(Document):
|
||||||
|
pass
|
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestGrievanceType(unittest.TestCase):
|
||||||
|
pass
|
@ -2,7 +2,7 @@
|
|||||||
// MIT License. See license.txt
|
// MIT License. See license.txt
|
||||||
|
|
||||||
frappe.listview_settings['Job Applicant'] = {
|
frappe.listview_settings['Job Applicant'] = {
|
||||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
add_fields: ["status"],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.status == "Accepted") {
|
if (doc.status == "Accepted") {
|
||||||
return [__(doc.status), "green", "status,=," + doc.status];
|
return [__(doc.status), "green", "status,=," + doc.status];
|
||||||
|
@ -110,6 +110,7 @@
|
|||||||
"label": "Allocation"
|
"label": "Allocation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
"fieldname": "new_leaves_allocated",
|
"fieldname": "new_leaves_allocated",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@ -235,7 +236,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-14 15:28:26.335104",
|
"modified": "2021-06-03 15:28:26.335104",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Allocation",
|
"name": "Leave Allocation",
|
||||||
|
@ -8,6 +8,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.hr.utils import set_employee_name, get_leave_period
|
from erpnext.hr.utils import set_employee_name, get_leave_period
|
||||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
|
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
|
||||||
|
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
|
||||||
|
|
||||||
class OverlapError(frappe.ValidationError): pass
|
class OverlapError(frappe.ValidationError): pass
|
||||||
class BackDatedAllocationError(frappe.ValidationError): pass
|
class BackDatedAllocationError(frappe.ValidationError): pass
|
||||||
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
|
|||||||
if self.carry_forward:
|
if self.carry_forward:
|
||||||
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
||||||
|
|
||||||
|
def on_update_after_submit(self):
|
||||||
|
if self.has_value_changed("new_leaves_allocated"):
|
||||||
|
self.validate_against_leave_applications()
|
||||||
|
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
||||||
|
args = {
|
||||||
|
"leaves": leaves_to_be_added,
|
||||||
|
"from_date": self.from_date,
|
||||||
|
"to_date": self.to_date,
|
||||||
|
"is_carry_forward": 0
|
||||||
|
}
|
||||||
|
create_leave_ledger_entry(self, args, True)
|
||||||
|
|
||||||
|
def get_existing_leave_count(self):
|
||||||
|
ledger_entries = frappe.get_all("Leave Ledger Entry",
|
||||||
|
filters={
|
||||||
|
"transaction_type": "Leave Allocation",
|
||||||
|
"transaction_name": self.name,
|
||||||
|
"employee": self.employee,
|
||||||
|
"company": self.company,
|
||||||
|
"leave_type": self.leave_type
|
||||||
|
},
|
||||||
|
pluck="leaves")
|
||||||
|
total_existing_leaves = 0
|
||||||
|
for entry in ledger_entries:
|
||||||
|
total_existing_leaves += entry
|
||||||
|
|
||||||
|
return total_existing_leaves
|
||||||
|
|
||||||
|
def validate_against_leave_applications(self):
|
||||||
|
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
|
||||||
|
self.from_date, self.to_date)
|
||||||
|
if flt(leaves_taken) > flt(self.total_leaves_allocated):
|
||||||
|
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||||
|
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
|
||||||
|
|
||||||
def update_leave_policy_assignments_when_no_allocations_left(self):
|
def update_leave_policy_assignments_when_no_allocations_left(self):
|
||||||
allocations = frappe.db.get_list("Leave Allocation", filters = {
|
allocations = frappe.db.get_list("Leave Allocation", filters = {
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
import erpnext
|
||||||
import unittest
|
import unittest
|
||||||
from frappe.utils import nowdate, add_months, getdate, add_days
|
from frappe.utils import nowdate, add_months, getdate, add_days
|
||||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||||
@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
leave_allocation.cancel()
|
leave_allocation.cancel()
|
||||||
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
||||||
|
|
||||||
|
def test_leave_addition_after_submit(self):
|
||||||
|
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||||
|
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||||
|
|
||||||
|
leave_allocation = create_leave_allocation()
|
||||||
|
leave_allocation.submit()
|
||||||
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
leave_allocation.new_leaves_allocated = 40
|
||||||
|
leave_allocation.submit()
|
||||||
|
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||||
|
|
||||||
|
def test_leave_subtraction_after_submit(self):
|
||||||
|
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||||
|
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||||
|
leave_allocation = create_leave_allocation()
|
||||||
|
leave_allocation.submit()
|
||||||
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
leave_allocation.new_leaves_allocated = 10
|
||||||
|
leave_allocation.submit()
|
||||||
|
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||||
|
|
||||||
|
def test_against_leave_application_validation_after_submit(self):
|
||||||
|
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||||
|
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||||
|
|
||||||
|
leave_allocation = create_leave_allocation()
|
||||||
|
leave_allocation.submit()
|
||||||
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||||
|
leave_application = frappe.get_doc({
|
||||||
|
"doctype": 'Leave Application',
|
||||||
|
"employee": employee.name,
|
||||||
|
"leave_type": "_Test Leave Type",
|
||||||
|
"from_date": add_months(nowdate(), 2),
|
||||||
|
"to_date": add_months(add_days(nowdate(), 10), 2),
|
||||||
|
"company": erpnext.get_default_company() or "_Test Company",
|
||||||
|
"docstatus": 1,
|
||||||
|
"status": "Approved",
|
||||||
|
"leave_approver": 'test@example.com'
|
||||||
|
})
|
||||||
|
leave_application.submit()
|
||||||
|
leave_allocation.new_leaves_allocated = 8
|
||||||
|
leave_allocation.total_leaves_allocated = 8
|
||||||
|
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
||||||
|
|
||||||
def create_leave_allocation(**args):
|
def create_leave_allocation(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class StaffingPlan(Document):
|
|||||||
|
|
||||||
detail.total_estimated_cost = 0
|
detail.total_estimated_cost = 0
|
||||||
if detail.number_of_positions > 0:
|
if detail.number_of_positions > 0:
|
||||||
if detail.vacancies > 0 and detail.estimated_cost_per_position:
|
if detail.vacancies and detail.estimated_cost_per_position:
|
||||||
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
|
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
|
||||||
|
|
||||||
self.total_estimated_budget += detail.total_estimated_cost
|
self.total_estimated_budget += detail.total_estimated_cost
|
||||||
@ -76,12 +76,12 @@ class StaffingPlan(Document):
|
|||||||
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
|
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
|
||||||
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
|
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
|
||||||
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
|
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
|
||||||
for {2} as per staffing plan {3} for parent company {4}."
|
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||||
.format(cint(parent_plan_details[0].vacancies),
|
cint(parent_plan_details[0].vacancies),
|
||||||
parent_plan_details[0].total_estimated_cost,
|
parent_plan_details[0].total_estimated_cost,
|
||||||
frappe.bold(staffing_plan_detail.designation),
|
frappe.bold(staffing_plan_detail.designation),
|
||||||
parent_plan_details[0].name,
|
parent_plan_details[0].name,
|
||||||
parent_company)), ParentCompanyError)
|
parent_company), ParentCompanyError)
|
||||||
|
|
||||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||||
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
|
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
|
||||||
@ -98,14 +98,14 @@ class StaffingPlan(Document):
|
|||||||
(flt(parent_plan_details[0].total_estimated_cost) < \
|
(flt(parent_plan_details[0].total_estimated_cost) < \
|
||||||
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
|
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
|
||||||
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
|
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
|
||||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||||
.format(cint(all_sibling_details.vacancies),
|
cint(all_sibling_details.vacancies),
|
||||||
all_sibling_details.total_estimated_cost,
|
all_sibling_details.total_estimated_cost,
|
||||||
frappe.bold(staffing_plan_detail.designation),
|
frappe.bold(staffing_plan_detail.designation),
|
||||||
parent_company,
|
parent_company,
|
||||||
cint(parent_plan_details[0].vacancies),
|
cint(parent_plan_details[0].vacancies),
|
||||||
parent_plan_details[0].total_estimated_cost,
|
parent_plan_details[0].total_estimated_cost,
|
||||||
parent_plan_details[0].name)))
|
parent_plan_details[0].name))
|
||||||
|
|
||||||
def validate_with_subsidiary_plans(self, staffing_plan_detail):
|
def validate_with_subsidiary_plans(self, staffing_plan_detail):
|
||||||
#Valdate this plan with all child company plan
|
#Valdate this plan with all child company plan
|
||||||
@ -121,11 +121,11 @@ class StaffingPlan(Document):
|
|||||||
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
|
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
|
||||||
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
|
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
|
||||||
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
|
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
|
||||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||||
.format(self.company,
|
self.company,
|
||||||
cint(children_details.vacancies),
|
cint(children_details.vacancies),
|
||||||
children_details.total_estimated_cost,
|
children_details.total_estimated_cost,
|
||||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_designation_counts(designation, company):
|
def get_designation_counts(designation, company):
|
||||||
|
@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
|
|||||||
frappe.set_route("List", "Training Feedback");
|
frappe.set_route("List", "Training Feedback");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
frm.events.set_employee_query(frm);
|
||||||
});
|
},
|
||||||
|
|
||||||
frappe.ui.form.on("Training Event Employee", {
|
set_employee_query: function(frm) {
|
||||||
employee: function (frm) {
|
|
||||||
let emp = [];
|
let emp = [];
|
||||||
for (let d in frm.doc.employees) {
|
for (let d in frm.doc.employees) {
|
||||||
if (frm.doc.employees[d].employee) {
|
if (frm.doc.employees[d].employee) {
|
||||||
@ -34,9 +33,17 @@ frappe.ui.form.on("Training Event Employee", {
|
|||||||
frm.set_query("employee", "employees", function () {
|
frm.set_query("employee", "employees", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
name: ["NOT IN", emp]
|
name: ["NOT IN", emp],
|
||||||
|
status: "Active"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("Training Event Employee", {
|
||||||
|
employee: function(frm) {
|
||||||
|
frm.events.set_employee_query(frm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Employee"
|
"options": "Employee"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,7 +69,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-21 12:41:59.336237",
|
"modified": "2021-07-02 17:20:27.630176",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Training Event Employee",
|
"name": "Training Event Employee",
|
||||||
|
@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
|
|||||||
is_carry_forward, is_expired
|
is_carry_forward, is_expired
|
||||||
FROM `tabLeave Ledger Entry`
|
FROM `tabLeave Ledger Entry`
|
||||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||||
AND docstatus=1 AND leaves>0
|
AND docstatus=1
|
||||||
AND (from_date between %(from_date)s AND %(to_date)s
|
AND (from_date between %(from_date)s AND %(to_date)s
|
||||||
OR to_date between %(from_date)s AND %(to_date)s
|
OR to_date between %(from_date)s AND %(to_date)s
|
||||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||||
|
@ -153,6 +153,24 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Grievance Type",
|
||||||
|
"link_to": "Grievance Type",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Employee Grievance",
|
||||||
|
"link_to": "Employee Grievance",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Employee",
|
"dependencies": "Employee",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -823,7 +841,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-04-26 13:36:15.413819",
|
"modified": "2021-05-13 17:19:40.524444",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR",
|
"name": "HR",
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Loan Security Pledge",
|
"options": "Loan Security Pledge",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "loan_application.applicant",
|
"fetch_from": "loan_application.applicant",
|
||||||
@ -45,47 +47,63 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Applicant",
|
"label": "Applicant",
|
||||||
"options": "applicant_type",
|
"options": "applicant_type",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "loan_security_details_section",
|
"fieldname": "loan_security_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Loan Security Details"
|
"label": "Loan Security Details",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "loan",
|
"fieldname": "loan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Loan",
|
"label": "Loan",
|
||||||
"options": "Loan"
|
"options": "Loan",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "loan_application",
|
"fieldname": "loan_application",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Loan Application",
|
"label": "Loan Application",
|
||||||
"options": "Loan Application"
|
"options": "Loan Application",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "total_security_value",
|
"fieldname": "total_security_value",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Total Security Value",
|
"label": "Total Security Value",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "maximum_loan_value",
|
"fieldname": "maximum_loan_value",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Maximum Loan Value",
|
"label": "Maximum Loan Value",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "loan_details_section",
|
"fieldname": "loan_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Loan Details"
|
"label": "Loan Details",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Requested",
|
"default": "Requested",
|
||||||
@ -94,37 +112,49 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged",
|
"options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "pledge_time",
|
"fieldname": "pledge_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Pledge Time",
|
"label": "Pledge Time",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "securities",
|
"fieldname": "securities",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Securities",
|
"label": "Securities",
|
||||||
"options": "Pledge",
|
"options": "Pledge",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_11",
|
"fieldname": "column_break_11",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_10",
|
"fieldname": "section_break_10",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Totals"
|
"label": "Totals",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "loan.applicant_type",
|
"fetch_from": "loan.applicant_type",
|
||||||
@ -132,35 +162,45 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Applicant Type",
|
"label": "Applicant Type",
|
||||||
"options": "Employee\nMember\nCustomer",
|
"options": "Employee\nMember\nCustomer",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "more_information_section",
|
"fieldname": "more_information_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "More Information"
|
"label": "More Information",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "reference_no",
|
"fieldname": "reference_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Reference No"
|
"label": "Reference No",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_18",
|
"fieldname": "column_break_18",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Description"
|
"label": "Description",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 18:23:16.953305",
|
"modified": "2021-06-29 17:15:16.082256",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Security Pledge",
|
"name": "Loan Security Pledge",
|
||||||
|
@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
|
|||||||
update_shortfall_status(self.loan, self.total_security_value)
|
update_shortfall_status(self.loan, self.total_security_value)
|
||||||
update_loan(self.loan, self.maximum_loan_value)
|
update_loan(self.loan, self.maximum_loan_value)
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
if self.loan:
|
||||||
|
self.db_set("status", "Cancelled")
|
||||||
|
self.db_set("pledge_time", None)
|
||||||
|
update_loan(self.loan, self.maximum_loan_value, cancel=1)
|
||||||
|
|
||||||
def validate_duplicate_securities(self):
|
def validate_duplicate_securities(self):
|
||||||
security_list = []
|
security_list = []
|
||||||
for security in self.securities:
|
for security in self.securities:
|
||||||
@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
|
|||||||
existing_pledge = ''
|
existing_pledge = ''
|
||||||
|
|
||||||
if self.loan:
|
if self.loan:
|
||||||
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name'])
|
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
|
||||||
|
|
||||||
if existing_pledge:
|
if existing_pledge:
|
||||||
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
|
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
|
||||||
@ -77,8 +83,12 @@ class LoanSecurityPledge(Document):
|
|||||||
self.total_security_value = total_security_value
|
self.total_security_value = total_security_value
|
||||||
self.maximum_loan_value = maximum_loan_value
|
self.maximum_loan_value = maximum_loan_value
|
||||||
|
|
||||||
def update_loan(loan, maximum_value_against_pledge):
|
def update_loan(loan, maximum_value_against_pledge, cancel=0):
|
||||||
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
|
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
|
||||||
|
|
||||||
|
if cancel:
|
||||||
|
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
|
||||||
|
WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
|
||||||
|
else:
|
||||||
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
|
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
|
||||||
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
|
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
|
||||||
|
@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
erpnext.hide_company();
|
erpnext.hide_company();
|
||||||
if (frm.doc.customer && frm.doc.docstatus === 1) {
|
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
|
||||||
frm.add_custom_button(__("Sales Order"), function() {
|
frm.add_custom_button(__("Sales Order"), function() {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
|
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2018-05-24 07:18:08.256060",
|
"creation": "2018-05-24 07:18:08.256060",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@ -79,6 +80,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "to_date",
|
"fieldname": "to_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "To Date",
|
"label": "To Date",
|
||||||
@ -129,8 +131,10 @@
|
|||||||
"label": "Terms and Conditions Details"
|
"label": "Terms and Conditions Details"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"modified": "2019-11-18 19:37:37.151686",
|
"links": [],
|
||||||
|
"modified": "2021-06-29 00:30:30.621636",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Blanket Order",
|
"name": "Blanket Order",
|
||||||
|
@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.toggle_enable("item", frm.doc.__islocal);
|
frm.toggle_enable("item", frm.doc.__islocal);
|
||||||
toggle_operations(frm);
|
|
||||||
|
|
||||||
frm.set_indicator_formatter('item_code',
|
frm.set_indicator_formatter('item_code',
|
||||||
function(doc) {
|
function(doc) {
|
||||||
@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
freeze: true,
|
freeze: true,
|
||||||
args: {
|
args: {
|
||||||
update_parent: true,
|
update_parent: true,
|
||||||
from_child_bom:false,
|
from_child_bom:false
|
||||||
save: frm.doc.docstatus === 1 ? true : false
|
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
refresh_field("items");
|
refresh_field("items");
|
||||||
@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
|
|||||||
erpnext.bom.calculate_total(frm.doc);
|
erpnext.bom.calculate_total(frm.doc);
|
||||||
});
|
});
|
||||||
|
|
||||||
var toggle_operations = function(frm) {
|
|
||||||
frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
|
|
||||||
frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
|
||||||
frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
frappe.ui.form.on("BOM", "with_operations", function(frm) {
|
frappe.ui.form.on("BOM", "with_operations", function(frm) {
|
||||||
if(!cint(frm.doc.with_operations)) {
|
if(!cint(frm.doc.with_operations)) {
|
||||||
frm.set_value("operations", []);
|
frm.set_value("operations", []);
|
||||||
}
|
}
|
||||||
toggle_operations(frm);
|
|
||||||
});
|
});
|
@ -36,6 +36,9 @@
|
|||||||
"materials_section",
|
"materials_section",
|
||||||
"inspection_required",
|
"inspection_required",
|
||||||
"quality_inspection_template",
|
"quality_inspection_template",
|
||||||
|
"column_break_31",
|
||||||
|
"bom_level",
|
||||||
|
"section_break_33",
|
||||||
"items",
|
"items",
|
||||||
"scrap_section",
|
"scrap_section",
|
||||||
"scrap_items",
|
"scrap_items",
|
||||||
@ -193,6 +196,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Work Order",
|
"default": "Work Order",
|
||||||
|
"depends_on": "with_operations",
|
||||||
"fieldname": "transfer_material_against",
|
"fieldname": "transfer_material_against",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Transfer Material Against",
|
"label": "Transfer Material Against",
|
||||||
@ -235,6 +239,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "operations_section",
|
"fieldname": "operations_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1,
|
||||||
"oldfieldtype": "Section Break"
|
"oldfieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -245,6 +250,7 @@
|
|||||||
"options": "Routing"
|
"options": "Routing"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "with_operations",
|
||||||
"fieldname": "operations",
|
"fieldname": "operations",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Operations",
|
"label": "Operations",
|
||||||
@ -510,6 +516,22 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_31",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "bom_level",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "BOM Level",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_33",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-sitemap",
|
"icon": "fa fa-sitemap",
|
||||||
@ -517,7 +539,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-21 12:29:32.634952",
|
"modified": "2021-05-16 12:25:09.081968",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from typing import List
|
||||||
|
from collections import deque
|
||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
from frappe.utils import cint, cstr, flt, today
|
from frappe.utils import cint, cstr, flt, today
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
form_grid_templates = {
|
form_grid_templates = {
|
||||||
"items": "templates/form_grid/item_grid.html"
|
"items": "templates/form_grid/item_grid.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BOMTree:
|
||||||
|
"""Full tree representation of a BOM"""
|
||||||
|
|
||||||
|
# specifying the attributes to save resources
|
||||||
|
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||||
|
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
||||||
|
|
||||||
|
def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
|
||||||
|
self.name = name # name of node, BOM number if is_bom else item_code
|
||||||
|
self.child_items: List["BOMTree"] = [] # list of child items
|
||||||
|
self.is_bom = is_bom # true if the node is a BOM and not a leaf item
|
||||||
|
self.item_code: str = None # item_code associated with node
|
||||||
|
self.qty = qty # required unit quantity to make one unit of parent item.
|
||||||
|
self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
|
||||||
|
if not self.is_bom:
|
||||||
|
self.item_code = self.name
|
||||||
|
else:
|
||||||
|
self.__create_tree()
|
||||||
|
|
||||||
|
def __create_tree(self):
|
||||||
|
bom = frappe.get_cached_doc("BOM", self.name)
|
||||||
|
self.item_code = bom.item
|
||||||
|
|
||||||
|
for item in bom.get("items", []):
|
||||||
|
qty = item.qty / bom.quantity # quantity per unit
|
||||||
|
exploded_qty = self.exploded_qty * qty
|
||||||
|
if item.bom_no:
|
||||||
|
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||||
|
self.child_items.append(child)
|
||||||
|
else:
|
||||||
|
self.child_items.append(
|
||||||
|
BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
|
||||||
|
)
|
||||||
|
|
||||||
|
def level_order_traversal(self) -> List["BOMTree"]:
|
||||||
|
"""Get level order traversal of tree.
|
||||||
|
E.g. for following tree the traversal will return list of nodes in order from top to bottom.
|
||||||
|
BOM:
|
||||||
|
- SubAssy1
|
||||||
|
- item1
|
||||||
|
- item2
|
||||||
|
- SubAssy2
|
||||||
|
- item3
|
||||||
|
- item4
|
||||||
|
|
||||||
|
returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
|
||||||
|
"""
|
||||||
|
traversal = []
|
||||||
|
q = deque()
|
||||||
|
q.append(self)
|
||||||
|
|
||||||
|
while q:
|
||||||
|
node = q.popleft()
|
||||||
|
|
||||||
|
for child in node.child_items:
|
||||||
|
traversal.append(child)
|
||||||
|
q.append(child)
|
||||||
|
|
||||||
|
return traversal
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
|
||||||
|
f" exploded_qty: {self.exploded_qty}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self, level: int = 0) -> str:
|
||||||
|
rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
|
||||||
|
for child in self.child_items:
|
||||||
|
rep += child.__repr__(level=level + 1)
|
||||||
|
return rep
|
||||||
|
|
||||||
class BOM(WebsiteGenerator):
|
class BOM(WebsiteGenerator):
|
||||||
website = frappe._dict(
|
website = frappe._dict(
|
||||||
# page_title_field = "item_name",
|
# page_title_field = "item_name",
|
||||||
@ -81,7 +153,8 @@ class BOM(WebsiteGenerator):
|
|||||||
self.validate_operations()
|
self.validate_operations()
|
||||||
self.calculate_cost()
|
self.calculate_cost()
|
||||||
self.update_stock_qty()
|
self.update_stock_qty()
|
||||||
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||||
|
self.set_bom_level()
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||||
@ -152,7 +225,7 @@ class BOM(WebsiteGenerator):
|
|||||||
if not args:
|
if not args:
|
||||||
args = frappe.form_dict.get('args')
|
args = frappe.form_dict.get('args')
|
||||||
|
|
||||||
if isinstance(args, string_types):
|
if isinstance(args, str):
|
||||||
import json
|
import json
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
@ -213,7 +286,7 @@ class BOM(WebsiteGenerator):
|
|||||||
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
|
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
|
def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -242,7 +315,7 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
self.flags.ignore_validate_update_after_submit = True
|
self.flags.ignore_validate_update_after_submit = True
|
||||||
self.calculate_cost()
|
self.calculate_cost(update_hour_rate)
|
||||||
if save:
|
if save:
|
||||||
self.db_update()
|
self.db_update()
|
||||||
|
|
||||||
@ -403,32 +476,47 @@ class BOM(WebsiteGenerator):
|
|||||||
bom_list.reverse()
|
bom_list.reverse()
|
||||||
return bom_list
|
return bom_list
|
||||||
|
|
||||||
def calculate_cost(self):
|
def calculate_cost(self, update_hour_rate = False):
|
||||||
"""Calculate bom totals"""
|
"""Calculate bom totals"""
|
||||||
self.calculate_op_cost()
|
self.calculate_op_cost(update_hour_rate)
|
||||||
self.calculate_rm_cost()
|
self.calculate_rm_cost()
|
||||||
self.calculate_sm_cost()
|
self.calculate_sm_cost()
|
||||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||||
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||||
|
|
||||||
def calculate_op_cost(self):
|
def calculate_op_cost(self, update_hour_rate = False):
|
||||||
"""Update workstation rate and calculates totals"""
|
"""Update workstation rate and calculates totals"""
|
||||||
self.operating_cost = 0
|
self.operating_cost = 0
|
||||||
self.base_operating_cost = 0
|
self.base_operating_cost = 0
|
||||||
for d in self.get('operations'):
|
for d in self.get('operations'):
|
||||||
if d.workstation:
|
if d.workstation:
|
||||||
if not d.hour_rate:
|
self.update_rate_and_time(d, update_hour_rate)
|
||||||
hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
|
|
||||||
d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
|
|
||||||
|
|
||||||
if d.hour_rate and d.time_in_mins:
|
|
||||||
d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
|
|
||||||
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
|
|
||||||
d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
|
|
||||||
|
|
||||||
self.operating_cost += flt(d.operating_cost)
|
self.operating_cost += flt(d.operating_cost)
|
||||||
self.base_operating_cost += flt(d.base_operating_cost)
|
self.base_operating_cost += flt(d.base_operating_cost)
|
||||||
|
|
||||||
|
def update_rate_and_time(self, row, update_hour_rate = False):
|
||||||
|
if not row.hour_rate or update_hour_rate:
|
||||||
|
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
|
||||||
|
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
||||||
|
if self.conversion_rate and hour_rate else hour_rate)
|
||||||
|
|
||||||
|
if self.routing:
|
||||||
|
row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
|
||||||
|
"workstation": row.workstation,
|
||||||
|
"operation": row.operation,
|
||||||
|
"sequence_id": row.sequence_id,
|
||||||
|
"parent": self.routing
|
||||||
|
}, ["time_in_mins"]))
|
||||||
|
|
||||||
|
if row.hour_rate and row.time_in_mins:
|
||||||
|
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
||||||
|
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
||||||
|
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
|
||||||
|
|
||||||
|
if update_hour_rate:
|
||||||
|
row.db_update()
|
||||||
|
|
||||||
def calculate_rm_cost(self):
|
def calculate_rm_cost(self):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
total_rm_cost = 0
|
total_rm_cost = 0
|
||||||
@ -575,7 +663,7 @@ class BOM(WebsiteGenerator):
|
|||||||
self.get_routing()
|
self.get_routing()
|
||||||
|
|
||||||
def validate_operations(self):
|
def validate_operations(self):
|
||||||
if self.with_operations and not self.get('operations'):
|
if self.with_operations and not self.get('operations') and self.docstatus == 1:
|
||||||
frappe.throw(_("Operations cannot be left blank"))
|
frappe.throw(_("Operations cannot be left blank"))
|
||||||
|
|
||||||
if self.with_operations:
|
if self.with_operations:
|
||||||
@ -585,6 +673,24 @@ class BOM(WebsiteGenerator):
|
|||||||
if not d.batch_size or d.batch_size <= 0:
|
if not d.batch_size or d.batch_size <= 0:
|
||||||
d.batch_size = 1
|
d.batch_size = 1
|
||||||
|
|
||||||
|
def get_tree_representation(self) -> BOMTree:
|
||||||
|
"""Get a complete tree representation preserving order of child items."""
|
||||||
|
return BOMTree(self.name)
|
||||||
|
|
||||||
|
def set_bom_level(self, update=False):
|
||||||
|
levels = []
|
||||||
|
|
||||||
|
self.bom_level = 0
|
||||||
|
for row in self.items:
|
||||||
|
if row.bom_no:
|
||||||
|
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
|
||||||
|
|
||||||
|
if levels:
|
||||||
|
self.bom_level = max(levels) + 1
|
||||||
|
|
||||||
|
if update:
|
||||||
|
self.db_set("bom_level", self.bom_level)
|
||||||
|
|
||||||
def get_bom_item_rate(args, bom_doc):
|
def get_bom_item_rate(args, bom_doc):
|
||||||
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
||||||
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
||||||
@ -768,7 +874,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
|||||||
frappe.form_dict.parent = parent
|
frappe.form_dict.parent = parent
|
||||||
|
|
||||||
if frappe.form_dict.parent:
|
if frappe.form_dict.parent:
|
||||||
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent)
|
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
|
||||||
frappe.has_permission("BOM", doc=bom_doc, throw=True)
|
frappe.has_permission("BOM", doc=bom_doc, throw=True)
|
||||||
|
|
||||||
bom_items = frappe.get_all('BOM Item',
|
bom_items = frappe.get_all('BOM Item',
|
||||||
@ -779,7 +885,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
|||||||
item_names = tuple(d.get('item_code') for d in bom_items)
|
item_names = tuple(d.get('item_code') for d in bom_items)
|
||||||
|
|
||||||
items = frappe.get_list('Item',
|
items = frappe.get_list('Item',
|
||||||
fields=['image', 'description', 'name', 'stock_uom', 'item_name'],
|
fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
|
||||||
filters=[['name', 'in', item_names]]) # to get only required item dicts
|
filters=[['name', 'in', item_names]]) # to get only required item dicts
|
||||||
|
|
||||||
for bom_item in bom_items:
|
for bom_item in bom_items:
|
||||||
@ -792,6 +898,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
|||||||
|
|
||||||
bom_item.parent_bom_qty = bom_doc.quantity
|
bom_item.parent_bom_qty = bom_doc.quantity
|
||||||
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
|
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
|
||||||
|
bom_item.image = frappe.db.escape(bom_item.image)
|
||||||
|
|
||||||
return bom_items
|
return bom_items
|
||||||
|
|
||||||
@ -1008,6 +1115,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
|
|||||||
},
|
},
|
||||||
'BOM Item': {
|
'BOM Item': {
|
||||||
'doctype': 'BOM Item',
|
'doctype': 'BOM Item',
|
||||||
|
# stop get_mapped_doc copying parent bom_no to children
|
||||||
|
'field_no_map': ['bom_no'],
|
||||||
'condition': lambda doc: doc.has_variants == 0
|
'condition': lambda doc: doc.has_variants == 0
|
||||||
},
|
},
|
||||||
}, target_doc, postprocess)
|
}, target_doc, postprocess)
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
<div style="padding: 15px;">
|
<div style="padding: 15px;">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-md-5" style="max-height: 500px">
|
||||||
{% if data.image %}
|
{% if data.image %}
|
||||||
|
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||||
<img class="responsive" src={{ data.image }}>
|
<img class="responsive" src={{ data.image }}>
|
||||||
<hr style="margin: 15px -15px;">
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7 h-500">
|
||||||
<h4>
|
<h4>
|
||||||
{{ __("Description") }}
|
{{ __("Description") }}
|
||||||
</h4>
|
</h4>
|
||||||
@ -10,6 +15,19 @@
|
|||||||
{{ data.description }}
|
{{ data.description }}
|
||||||
</div>
|
</div>
|
||||||
<hr style="margin: 15px -15px;">
|
<hr style="margin: 15px -15px;">
|
||||||
|
<p>
|
||||||
|
{% if data.value %}
|
||||||
|
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
|
||||||
|
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if data.item_code %}
|
||||||
|
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
|
||||||
|
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr style="margin: 15px -15px;">
|
||||||
<p>
|
<p>
|
||||||
{% if data.value %}
|
{% if data.value %}
|
||||||
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/Form/BOM/{{ data.value }}">
|
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/Form/BOM/{{ data.value }}">
|
||||||
|
@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
|
|||||||
if(node.is_root && node.data.value!="BOM") {
|
if(node.is_root && node.data.value!="BOM") {
|
||||||
frappe.model.with_doc("BOM", node.data.value, function() {
|
frappe.model.with_doc("BOM", node.data.value, function() {
|
||||||
var bom = frappe.model.get_doc("BOM", node.data.value);
|
var bom = frappe.model.get_doc("BOM", node.data.value);
|
||||||
node.data.image = bom.image || "";
|
node.data.image = escape(bom.image) || "";
|
||||||
node.data.description = bom.description || "";
|
node.data.description = bom.description || "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,13 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from collections import deque
|
||||||
import unittest
|
import unittest
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||||
from six import string_types
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||||
@ -123,7 +122,7 @@ class TestBOM(unittest.TestCase):
|
|||||||
bom.items[0].conversion_factor = 5
|
bom.items[0].conversion_factor = 5
|
||||||
bom.insert()
|
bom.insert()
|
||||||
|
|
||||||
bom.update_cost()
|
bom.update_cost(update_hour_rate = False)
|
||||||
|
|
||||||
# test amounts in selected currency
|
# test amounts in selected currency
|
||||||
self.assertEqual(bom.items[0].rate, 300)
|
self.assertEqual(bom.items[0].rate, 300)
|
||||||
@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase):
|
|||||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||||
self.assertEqual(bom_items, supplied_items)
|
self.assertEqual(bom_items, supplied_items)
|
||||||
|
|
||||||
|
def test_bom_tree_representation(self):
|
||||||
|
bom_tree = {
|
||||||
|
"Assembly": {
|
||||||
|
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
|
||||||
|
"SubAssembly2": {"ChildPart3": {}},
|
||||||
|
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
|
||||||
|
"ChildPart5": {},
|
||||||
|
"ChildPart6": {},
|
||||||
|
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||||
|
created_tree = parent_bom.get_tree_representation()
|
||||||
|
|
||||||
|
reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
|
||||||
|
created_order = created_tree.level_order_traversal()
|
||||||
|
|
||||||
|
self.assertEqual(len(reqd_order), len(created_order))
|
||||||
|
|
||||||
|
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||||
|
self.assertEqual(reqd_item, created_item.item_code)
|
||||||
|
|
||||||
|
|
||||||
def get_default_bom(item_code="_Test FG Item 2"):
|
def get_default_bom(item_code="_Test FG Item 2"):
|
||||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def level_order_traversal(node):
|
||||||
|
traversal = []
|
||||||
|
q = deque()
|
||||||
|
q.append(node)
|
||||||
|
|
||||||
|
while q:
|
||||||
|
node = q.popleft()
|
||||||
|
|
||||||
|
for node_name, subtree in node.items():
|
||||||
|
traversal.append(node_name)
|
||||||
|
q.append(subtree)
|
||||||
|
|
||||||
|
return traversal
|
||||||
|
|
||||||
|
def create_nested_bom(tree, prefix="_Test bom "):
|
||||||
|
""" Helper function to create a simple nested bom from tree describing item names. (along with required items)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_items(bom_tree):
|
||||||
|
for item_code, subtree in bom_tree.items():
|
||||||
|
bom_item_code = prefix + item_code
|
||||||
|
if not frappe.db.exists("Item", bom_item_code):
|
||||||
|
frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
|
||||||
|
create_items(subtree)
|
||||||
|
create_items(tree)
|
||||||
|
|
||||||
|
def dfs(tree, node):
|
||||||
|
"""naive implementation for searching right subtree"""
|
||||||
|
for node_name, subtree in tree.items():
|
||||||
|
if node_name == node:
|
||||||
|
return subtree
|
||||||
|
else:
|
||||||
|
result = dfs(subtree, node)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
order_of_creating_bom = reversed(level_order_traversal(tree))
|
||||||
|
|
||||||
|
for item in order_of_creating_bom:
|
||||||
|
child_items = dfs(tree, item)
|
||||||
|
if child_items:
|
||||||
|
bom_item_code = prefix + item
|
||||||
|
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
|
||||||
|
for child_item in child_items.keys():
|
||||||
|
bom.append("items", {"item_code": prefix + child_item})
|
||||||
|
bom.insert()
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
|
return bom # parent bom is last bom
|
||||||
|
|
||||||
|
|
||||||
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
|
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
|
||||||
if warehouse_list and isinstance(warehouse_list, string_types):
|
if warehouse_list and isinstance(warehouse_list, str):
|
||||||
warehouse_list = [warehouse_list]
|
warehouse_list = [warehouse_list]
|
||||||
|
|
||||||
if not warehouse_list:
|
if not warehouse_list:
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
"col_break1",
|
"col_break1",
|
||||||
"hour_rate",
|
"hour_rate",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
"batch_size",
|
|
||||||
"operating_cost",
|
"operating_cost",
|
||||||
"base_hour_rate",
|
"base_hour_rate",
|
||||||
"base_operating_cost",
|
"base_operating_cost",
|
||||||
|
"batch_size",
|
||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -61,6 +61,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "In minutes",
|
"description": "In minutes",
|
||||||
|
"fetch_from": "operation.total_operation_time",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -104,7 +106,8 @@
|
|||||||
"label": "Image"
|
"label": "Image"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"fetch_from": "operation.batch_size",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "batch_size",
|
"fieldname": "batch_size",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Batch Size"
|
"label": "Batch Size"
|
||||||
@ -120,7 +123,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-13 18:14:10.018774",
|
"modified": "2021-01-12 14:48:09.596843",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
|
@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_indicator_formatter('sub_operation',
|
||||||
|
function(doc) {
|
||||||
|
if (doc.status == "Pending") {
|
||||||
|
return "red";
|
||||||
|
} else {
|
||||||
|
return doc.status === "Complete" ? "green" : "orange";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
|
||||||
|
frm.trigger('setup_corrective_job_card');
|
||||||
|
}
|
||||||
|
|
||||||
frm.set_query("quality_inspection", function() {
|
frm.set_query("quality_inspection", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
|
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
|
||||||
@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
|
|||||||
|
|
||||||
frm.trigger("toggle_operation_number");
|
frm.trigger("toggle_operation_number");
|
||||||
|
|
||||||
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
||||||
|
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||||
frm.trigger("prepare_timer_buttons");
|
frm.trigger("prepare_timer_buttons");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setup_corrective_job_card: function(frm) {
|
||||||
|
frm.add_custom_button(__('Corrective Job Card'), () => {
|
||||||
|
let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
|
||||||
|
|
||||||
|
let fields = [
|
||||||
|
{
|
||||||
|
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
|
||||||
|
fieldname: 'operation', get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"is_corrective_operation": 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
|
||||||
|
fieldname: 'for_operation', get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"name": ["in", operations]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.prompt(fields, d => {
|
||||||
|
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
|
||||||
|
}, __("Select Corrective Operation"));
|
||||||
|
}, __('Make'));
|
||||||
|
},
|
||||||
|
|
||||||
|
make_corrective_job_card: function(frm, operation, for_operation) {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
|
||||||
|
args: {
|
||||||
|
source_name: frm.doc.name,
|
||||||
|
operation: operation,
|
||||||
|
for_operation: for_operation
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
operation: function(frm) {
|
operation: function(frm) {
|
||||||
frm.trigger("toggle_operation_number");
|
frm.trigger("toggle_operation_number");
|
||||||
|
|
||||||
@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
|
|||||||
|
|
||||||
prepare_timer_buttons: function(frm) {
|
prepare_timer_buttons: function(frm) {
|
||||||
frm.trigger("make_dashboard");
|
frm.trigger("make_dashboard");
|
||||||
if (!frm.doc.job_started) {
|
|
||||||
frm.add_custom_button(__("Start"), () => {
|
if (!frm.doc.started_time && !frm.doc.current_time) {
|
||||||
if (!frm.doc.employee) {
|
frm.add_custom_button(__("Start Job"), () => {
|
||||||
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
|
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
|
||||||
fieldname: 'employee'}, d => {
|
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
|
||||||
if (d.employee) {
|
options: "Job Card Time Log", fieldname: 'employees'}, d => {
|
||||||
frm.set_value("employee", d.employee);
|
frm.events.start_job(frm, "Work In Progress", d.employees);
|
||||||
|
}, __("Assign Job to Employee"));
|
||||||
} else {
|
} else {
|
||||||
frm.events.start_job(frm);
|
frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
|
||||||
}
|
|
||||||
}, __("Enter Value"), __("Start"));
|
|
||||||
} else {
|
|
||||||
frm.events.start_job(frm);
|
|
||||||
}
|
}
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
} else if (frm.doc.status == "On Hold") {
|
} else if (frm.doc.status == "On Hold") {
|
||||||
frm.add_custom_button(__("Resume"), () => {
|
frm.add_custom_button(__("Resume Job"), () => {
|
||||||
frappe.flags.resume_job = 1;
|
frm.events.start_job(frm, "Resume Job", frm.doc.employee);
|
||||||
frm.events.start_job(frm);
|
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
} else {
|
} else {
|
||||||
frm.add_custom_button(__("Pause"), () => {
|
frm.add_custom_button(__("Pause Job"), () => {
|
||||||
frappe.flags.pause_job = 1;
|
frm.events.complete_job(frm, "On Hold");
|
||||||
frm.set_value("status", "On Hold");
|
|
||||||
frm.events.complete_job(frm);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.add_custom_button(__("Complete"), () => {
|
frm.add_custom_button(__("Complete Job"), () => {
|
||||||
let completed_time = frappe.datetime.now_datetime();
|
var sub_operations = frm.doc.sub_operations;
|
||||||
frm.trigger("hide_timer");
|
|
||||||
|
|
||||||
if (frm.doc.for_quantity) {
|
let set_qty = true;
|
||||||
|
if (sub_operations && sub_operations.length > 1) {
|
||||||
|
set_qty = false;
|
||||||
|
let last_op_row = sub_operations[sub_operations.length - 2];
|
||||||
|
|
||||||
|
if (last_op_row.status == 'Complete') {
|
||||||
|
set_qty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set_qty) {
|
||||||
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
|
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
|
||||||
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
|
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
|
||||||
frm.events.complete_job(frm, completed_time, data.qty);
|
frm.events.complete_job(frm, "Complete", data.qty);
|
||||||
}, __("Enter Value"), __("Complete"));
|
}, __("Enter Value"));
|
||||||
} else {
|
} else {
|
||||||
frm.events.complete_job(frm, completed_time, 0);
|
frm.events.complete_job(frm, "Complete", 0.0);
|
||||||
}
|
}
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start_job: function(frm) {
|
start_job: function(frm, status, employee) {
|
||||||
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
|
const args = {
|
||||||
row.from_time = frappe.datetime.now_datetime();
|
job_card_id: frm.doc.name,
|
||||||
frm.set_value('job_started', 1);
|
start_time: frappe.datetime.now_datetime(),
|
||||||
frm.set_value('started_time' , row.from_time);
|
employees: employee,
|
||||||
frm.set_value("status", "Work In Progress");
|
status: status
|
||||||
|
};
|
||||||
if (!frappe.flags.resume_job) {
|
frm.events.make_time_log(frm, args);
|
||||||
frm.set_value('current_time' , 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
frm.save();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
complete_job: function(frm, completed_time, completed_qty) {
|
complete_job: function(frm, status, completed_qty) {
|
||||||
frm.doc.time_logs.forEach(d => {
|
const args = {
|
||||||
if (d.from_time && !d.to_time) {
|
job_card_id: frm.doc.name,
|
||||||
d.to_time = completed_time || frappe.datetime.now_datetime();
|
complete_time: frappe.datetime.now_datetime(),
|
||||||
d.completed_qty = completed_qty || 0;
|
status: status,
|
||||||
|
completed_qty: completed_qty
|
||||||
|
};
|
||||||
|
frm.events.make_time_log(frm, args);
|
||||||
|
},
|
||||||
|
|
||||||
if(frappe.flags.pause_job) {
|
make_time_log: function(frm, args) {
|
||||||
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
|
frm.events.update_sub_operation(frm, args);
|
||||||
frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
|
|
||||||
} else {
|
|
||||||
frm.set_value('started_time' , '');
|
|
||||||
frm.set_value('job_started', 0);
|
|
||||||
frm.set_value('current_time' , 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
frm.save();
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
|
||||||
|
args: {
|
||||||
|
args: args
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function () {
|
||||||
|
frm.reload_doc();
|
||||||
|
frm.trigger("make_dashboard");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
update_sub_operation: function(frm, args) {
|
||||||
|
if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
|
||||||
|
let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
|
||||||
|
if (sub_operations && sub_operations.length) {
|
||||||
|
args["sub_operation"] = sub_operations[0].sub_operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
validate: function(frm) {
|
validate: function(frm) {
|
||||||
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
|
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
|
||||||
frm.trigger("reset_timer");
|
frm.trigger("reset_timer");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
employee: function(frm) {
|
|
||||||
if (frm.doc.job_started && !frm.doc.current_time) {
|
|
||||||
frm.trigger("reset_timer");
|
|
||||||
} else {
|
|
||||||
frm.events.start_job(frm);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reset_timer: function(frm) {
|
reset_timer: function(frm) {
|
||||||
frm.set_value('started_time' , '');
|
frm.set_value('started_time' , '');
|
||||||
frm.set_value('job_started', 0);
|
|
||||||
frm.set_value('current_time' , 0);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
make_dashboard: function(frm) {
|
make_dashboard: function(frm) {
|
||||||
@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
to_time: function(frm) {
|
to_time: function(frm) {
|
||||||
frm.set_value('job_started', 0);
|
|
||||||
frm.set_value('started_time', '');
|
frm.set_value('started_time', '');
|
||||||
}
|
}
|
||||||
})
|
})
|
@ -9,38 +9,49 @@
|
|||||||
"naming_series",
|
"naming_series",
|
||||||
"work_order",
|
"work_order",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
"workstation",
|
|
||||||
"operation",
|
|
||||||
"operation_row_number",
|
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"company",
|
"company",
|
||||||
"remarks",
|
|
||||||
"production_section",
|
"production_section",
|
||||||
"production_item",
|
"production_item",
|
||||||
"item_name",
|
"item_name",
|
||||||
"for_quantity",
|
"for_quantity",
|
||||||
"quality_inspection",
|
"serial_no",
|
||||||
"wip_warehouse",
|
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"employee",
|
"wip_warehouse",
|
||||||
"employee_name",
|
"quality_inspection",
|
||||||
"status",
|
|
||||||
"project",
|
"project",
|
||||||
|
"batch_no",
|
||||||
|
"operation_section_section",
|
||||||
|
"operation",
|
||||||
|
"operation_row_number",
|
||||||
|
"column_break_18",
|
||||||
|
"workstation",
|
||||||
|
"employee",
|
||||||
|
"section_break_21",
|
||||||
|
"sub_operations",
|
||||||
"timing_detail",
|
"timing_detail",
|
||||||
"time_logs",
|
"time_logs",
|
||||||
"section_break_13",
|
"section_break_13",
|
||||||
"total_completed_qty",
|
"total_completed_qty",
|
||||||
"total_time_in_mins",
|
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
|
"total_time_in_mins",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"items",
|
"items",
|
||||||
|
"corrective_operation_section",
|
||||||
|
"for_job_card",
|
||||||
|
"is_corrective_job_card",
|
||||||
|
"column_break_33",
|
||||||
|
"hour_rate",
|
||||||
|
"for_operation",
|
||||||
"more_information",
|
"more_information",
|
||||||
"operation_id",
|
"operation_id",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"transferred_qty",
|
"transferred_qty",
|
||||||
"requested_qty",
|
"requested_qty",
|
||||||
|
"status",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
|
"remarks",
|
||||||
"barcode",
|
"barcode",
|
||||||
"job_started",
|
"job_started",
|
||||||
"started_time",
|
"started_time",
|
||||||
@ -117,13 +128,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Timing Detail"
|
"label": "Timing Detail"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "employee",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Employee",
|
|
||||||
"options": "Employee"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 1,
|
"allow_bulk_edit": 1,
|
||||||
"fieldname": "time_logs",
|
"fieldname": "time_logs",
|
||||||
@ -133,9 +137,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "section_break_13",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "0",
|
||||||
"fieldname": "total_completed_qty",
|
"fieldname": "total_completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Total Completed Qty",
|
"label": "Total Completed Qty",
|
||||||
@ -160,8 +166,7 @@
|
|||||||
"fieldname": "items",
|
"fieldname": "items",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Items",
|
"label": "Items",
|
||||||
"options": "Job Card Item",
|
"options": "Job Card Item"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@ -251,12 +256,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "employee.employee_name",
|
"collapsible": 1,
|
||||||
"fieldname": "employee_name",
|
|
||||||
"fieldtype": "Read Only",
|
|
||||||
"label": "Employee Name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "production_section",
|
"fieldname": "production_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Production"
|
"label": "Production"
|
||||||
@ -314,11 +314,89 @@
|
|||||||
"label": "Quality Inspection",
|
"label": "Quality Inspection",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Quality Inspection"
|
"options": "Quality Inspection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 1,
|
||||||
|
"fieldname": "sub_operations",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Sub Operations",
|
||||||
|
"options": "Job Card Operation",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation_section_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Operation Section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_18",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_21",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "is_corrective_job_card",
|
||||||
|
"fieldname": "hour_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Hour Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"depends_on": "is_corrective_job_card",
|
||||||
|
"fieldname": "corrective_operation_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Corrective Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_corrective_job_card",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Corrective Job Card",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_33",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "for_job_card",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "For Job Card",
|
||||||
|
"options": "Job Card",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "for_job_card.operation",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "for_operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "For Operation",
|
||||||
|
"options": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "employee",
|
||||||
|
"fieldtype": "Table MultiSelect",
|
||||||
|
"label": "Employee",
|
||||||
|
"options": "Job Card Time Log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serial_no",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Serial No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Batch No",
|
||||||
|
"options": "Batch"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-19 18:26:50.531664",
|
"modified": "2021-03-16 15:59:32.766484",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
@ -5,11 +5,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
||||||
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
|
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
|
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
|
||||||
|
|
||||||
@ -25,10 +26,21 @@ class JobCard(Document):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_operation_id()
|
self.validate_operation_id()
|
||||||
self.validate_sequence_id()
|
self.validate_sequence_id()
|
||||||
|
self.get_sub_operations()
|
||||||
|
self.update_sub_operation_status()
|
||||||
|
|
||||||
|
def get_sub_operations(self):
|
||||||
|
if self.operation:
|
||||||
|
self.sub_operations = []
|
||||||
|
for row in frappe.get_all("Sub Operation",
|
||||||
|
filters = {"parent": self.operation}, fields=["operation", "idx"]):
|
||||||
|
row.status = "Pending"
|
||||||
|
row.sub_operation = row.operation
|
||||||
|
self.append("sub_operations", row)
|
||||||
|
|
||||||
def validate_time_logs(self):
|
def validate_time_logs(self):
|
||||||
self.total_completed_qty = 0.0
|
|
||||||
self.total_time_in_mins = 0.0
|
self.total_time_in_mins = 0.0
|
||||||
|
self.total_completed_qty = 0.0
|
||||||
|
|
||||||
if self.get('time_logs'):
|
if self.get('time_logs'):
|
||||||
for d in self.get('time_logs'):
|
for d in self.get('time_logs'):
|
||||||
@ -44,11 +56,14 @@ class JobCard(Document):
|
|||||||
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
|
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
|
||||||
self.total_time_in_mins += d.time_in_mins
|
self.total_time_in_mins += d.time_in_mins
|
||||||
|
|
||||||
if d.completed_qty:
|
if d.completed_qty and not self.sub_operations:
|
||||||
self.total_completed_qty += d.completed_qty
|
self.total_completed_qty += d.completed_qty
|
||||||
|
|
||||||
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
||||||
|
|
||||||
|
for row in self.sub_operations:
|
||||||
|
self.total_completed_qty += row.completed_qty
|
||||||
|
|
||||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||||
production_capacity = 1
|
production_capacity = 1
|
||||||
|
|
||||||
@ -57,7 +72,7 @@ class JobCard(Document):
|
|||||||
self.workstation, 'production_capacity') or 1
|
self.workstation, 'production_capacity') or 1
|
||||||
validate_overlap_for = " and jc.workstation = %(workstation)s "
|
validate_overlap_for = " and jc.workstation = %(workstation)s "
|
||||||
|
|
||||||
if self.employee:
|
if args.get("employee"):
|
||||||
# override capacity for employee
|
# override capacity for employee
|
||||||
production_capacity = 1
|
production_capacity = 1
|
||||||
validate_overlap_for = " and jc.employee = %(employee)s "
|
validate_overlap_for = " and jc.employee = %(employee)s "
|
||||||
@ -80,7 +95,7 @@ class JobCard(Document):
|
|||||||
"to_time": args.to_time,
|
"to_time": args.to_time,
|
||||||
"name": args.name or "No Name",
|
"name": args.name or "No Name",
|
||||||
"parent": args.parent or "No Name",
|
"parent": args.parent or "No Name",
|
||||||
"employee": self.employee,
|
"employee": args.get("employee"),
|
||||||
"workstation": self.workstation
|
"workstation": self.workstation
|
||||||
}, as_dict=True)
|
}, as_dict=True)
|
||||||
|
|
||||||
@ -158,6 +173,108 @@ class JobCard(Document):
|
|||||||
row.planned_start_time = datetime.datetime.combine(start_date,
|
row.planned_start_time = datetime.datetime.combine(start_date,
|
||||||
get_time(workstation_doc.working_hours[0].start_time))
|
get_time(workstation_doc.working_hours[0].start_time))
|
||||||
|
|
||||||
|
def add_time_log(self, args):
|
||||||
|
last_row = []
|
||||||
|
employees = args.employees
|
||||||
|
if isinstance(employees, str):
|
||||||
|
employees = json.loads(employees)
|
||||||
|
|
||||||
|
if self.time_logs and len(self.time_logs) > 0:
|
||||||
|
last_row = self.time_logs[-1]
|
||||||
|
|
||||||
|
self.reset_timer_value(args)
|
||||||
|
if last_row and args.get("complete_time"):
|
||||||
|
for row in self.time_logs:
|
||||||
|
if not row.to_time:
|
||||||
|
row.update({
|
||||||
|
"to_time": get_datetime(args.get("complete_time")),
|
||||||
|
"operation": args.get("sub_operation"),
|
||||||
|
"completed_qty": args.get("completed_qty") or 0.0
|
||||||
|
})
|
||||||
|
elif args.get("start_time"):
|
||||||
|
new_args = {
|
||||||
|
"from_time": get_datetime(args.get("start_time")),
|
||||||
|
"operation": args.get("sub_operation"),
|
||||||
|
"completed_qty": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if employees:
|
||||||
|
for name in employees:
|
||||||
|
new_args.employee = name.get('employee')
|
||||||
|
self.add_start_time_log(new_args)
|
||||||
|
else:
|
||||||
|
self.add_start_time_log(new_args)
|
||||||
|
|
||||||
|
if not self.employee and employees:
|
||||||
|
self.set_employees(employees)
|
||||||
|
|
||||||
|
if self.status == "On Hold":
|
||||||
|
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def add_start_time_log(self, args):
|
||||||
|
self.append("time_logs", args)
|
||||||
|
|
||||||
|
def set_employees(self, employees):
|
||||||
|
for name in employees:
|
||||||
|
self.append('employee', {
|
||||||
|
'employee': name.get('employee'),
|
||||||
|
'completed_qty': 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
def reset_timer_value(self, args):
|
||||||
|
self.started_time = None
|
||||||
|
|
||||||
|
if args.get("status") in ["Work In Progress", "Complete"]:
|
||||||
|
self.current_time = 0.0
|
||||||
|
|
||||||
|
if args.get("status") == "Work In Progress":
|
||||||
|
self.started_time = get_datetime(args.get("start_time"))
|
||||||
|
|
||||||
|
if args.get("status") == "Resume Job":
|
||||||
|
args["status"] = "Work In Progress"
|
||||||
|
|
||||||
|
if args.get("status"):
|
||||||
|
self.status = args.get("status")
|
||||||
|
|
||||||
|
def update_sub_operation_status(self):
|
||||||
|
if not (self.sub_operations and self.time_logs):
|
||||||
|
return
|
||||||
|
|
||||||
|
operation_wise_completed_time = {}
|
||||||
|
for time_log in self.time_logs:
|
||||||
|
if time_log.operation not in operation_wise_completed_time:
|
||||||
|
operation_wise_completed_time.setdefault(time_log.operation,
|
||||||
|
frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
|
||||||
|
|
||||||
|
op_row = operation_wise_completed_time[time_log.operation]
|
||||||
|
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
|
||||||
|
if self.status == 'On Hold':
|
||||||
|
op_row.status = 'Pause'
|
||||||
|
|
||||||
|
op_row.employee.append(time_log.employee)
|
||||||
|
if time_log.time_in_mins:
|
||||||
|
op_row.completed_time += time_log.time_in_mins
|
||||||
|
op_row.completed_qty += time_log.completed_qty
|
||||||
|
|
||||||
|
for row in self.sub_operations:
|
||||||
|
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
|
||||||
|
if operation_deatils:
|
||||||
|
if row.status != 'Complete':
|
||||||
|
row.status = operation_deatils.status
|
||||||
|
|
||||||
|
row.completed_time = operation_deatils.completed_time
|
||||||
|
if operation_deatils.employee:
|
||||||
|
row.completed_time = row.completed_time / len(set(operation_deatils.employee))
|
||||||
|
|
||||||
|
if operation_deatils.completed_qty:
|
||||||
|
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
|
||||||
|
else:
|
||||||
|
row.status = 'Pending'
|
||||||
|
row.completed_time = 0.0
|
||||||
|
row.completed_qty = 0.0
|
||||||
|
|
||||||
def update_time_logs(self, row):
|
def update_time_logs(self, row):
|
||||||
self.append("time_logs", {
|
self.append("time_logs", {
|
||||||
"from_time": row.planned_start_time,
|
"from_time": row.planned_start_time,
|
||||||
@ -182,15 +299,18 @@ class JobCard(Document):
|
|||||||
|
|
||||||
if self.get('operation') == d.operation:
|
if self.get('operation') == d.operation:
|
||||||
self.append('items', {
|
self.append('items', {
|
||||||
'item_code': d.item_code,
|
"item_code": d.item_code,
|
||||||
'source_warehouse': d.source_warehouse,
|
"source_warehouse": d.source_warehouse,
|
||||||
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
||||||
'item_name': d.item_name,
|
"item_name": d.item_name,
|
||||||
'description': d.description,
|
"description": d.description,
|
||||||
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
|
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
|
||||||
|
"rate": d.rate,
|
||||||
|
"amount": d.amount
|
||||||
})
|
})
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.validate_transfer_qty()
|
||||||
self.validate_job_card()
|
self.validate_job_card()
|
||||||
self.update_work_order()
|
self.update_work_order()
|
||||||
self.set_transferred_qty()
|
self.set_transferred_qty()
|
||||||
@ -199,7 +319,16 @@ class JobCard(Document):
|
|||||||
self.update_work_order()
|
self.update_work_order()
|
||||||
self.set_transferred_qty()
|
self.set_transferred_qty()
|
||||||
|
|
||||||
|
def validate_transfer_qty(self):
|
||||||
|
if self.items and self.transferred_qty < self.for_quantity:
|
||||||
|
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
|
||||||
|
.format(self.name))
|
||||||
|
|
||||||
def validate_job_card(self):
|
def validate_job_card(self):
|
||||||
|
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
|
||||||
|
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
|
||||||
|
.format(get_link_to_form('Work Order', self.work_order)))
|
||||||
|
|
||||||
if not self.time_logs:
|
if not self.time_logs:
|
||||||
frappe.throw(_("Time logs are required for {0} {1}")
|
frappe.throw(_("Time logs are required for {0} {1}")
|
||||||
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
||||||
@ -215,6 +344,10 @@ class JobCard(Document):
|
|||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
|
||||||
|
'add_corrective_operation_cost_in_finished_good_valuation')):
|
||||||
|
return
|
||||||
|
|
||||||
for_quantity, time_in_mins = 0, 0
|
for_quantity, time_in_mins = 0, 0
|
||||||
from_time_list, to_time_list = [], []
|
from_time_list, to_time_list = [], []
|
||||||
|
|
||||||
@ -225,10 +358,24 @@ class JobCard(Document):
|
|||||||
time_in_mins = flt(data[0].time_in_mins)
|
time_in_mins = flt(data[0].time_in_mins)
|
||||||
|
|
||||||
wo = frappe.get_doc('Work Order', self.work_order)
|
wo = frappe.get_doc('Work Order', self.work_order)
|
||||||
if self.operation_id:
|
|
||||||
|
if self.is_corrective_job_card:
|
||||||
|
self.update_corrective_in_work_order(wo)
|
||||||
|
|
||||||
|
elif self.operation_id:
|
||||||
self.validate_produced_quantity(for_quantity, wo)
|
self.validate_produced_quantity(for_quantity, wo)
|
||||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||||
|
|
||||||
|
def update_corrective_in_work_order(self, wo):
|
||||||
|
wo.corrective_operation_cost = 0.0
|
||||||
|
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
|
||||||
|
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
|
||||||
|
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
|
||||||
|
|
||||||
|
wo.calculate_operating_cost()
|
||||||
|
wo.flags.ignore_validate_update_after_submit = True
|
||||||
|
wo.save()
|
||||||
|
|
||||||
def validate_produced_quantity(self, for_quantity, wo):
|
def validate_produced_quantity(self, for_quantity, wo):
|
||||||
if self.docstatus < 2: return
|
if self.docstatus < 2: return
|
||||||
|
|
||||||
@ -248,8 +395,8 @@ class JobCard(Document):
|
|||||||
min(from_time) as start_time, max(to_time) as end_time
|
min(from_time) as start_time, max(to_time) as end_time
|
||||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||||
WHERE
|
WHERE
|
||||||
jctl.parent = jc.name and jc.work_order = %s
|
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
|
||||||
and jc.operation_id = %s and jc.docstatus = 1
|
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
|
||||||
""", (self.work_order, self.operation_id), as_dict=1)
|
""", (self.work_order, self.operation_id), as_dict=1)
|
||||||
|
|
||||||
for data in wo.operations:
|
for data in wo.operations:
|
||||||
@ -271,7 +418,8 @@ class JobCard(Document):
|
|||||||
def get_current_operation_data(self):
|
def get_current_operation_data(self):
|
||||||
return frappe.get_all('Job Card',
|
return frappe.get_all('Job Card',
|
||||||
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||||
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
|
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
|
||||||
|
"is_corrective_job_card": 0})
|
||||||
|
|
||||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
def set_transferred_qty_in_job_card(self, ste_doc):
|
||||||
for row in ste_doc.items:
|
for row in ste_doc.items:
|
||||||
@ -354,7 +502,11 @@ class JobCard(Document):
|
|||||||
.format(bold(self.operation), work_order), OperationMismatchError)
|
.format(bold(self.operation), work_order), OperationMismatchError)
|
||||||
|
|
||||||
def validate_sequence_id(self):
|
def validate_sequence_id(self):
|
||||||
if not (self.work_order and self.sequence_id): return
|
if self.is_corrective_job_card:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (self.work_order and self.sequence_id):
|
||||||
|
return
|
||||||
|
|
||||||
current_operation_qty = 0.0
|
current_operation_qty = 0.0
|
||||||
data = self.get_current_operation_data()
|
data = self.get_current_operation_data()
|
||||||
@ -376,6 +528,17 @@ class JobCard(Document):
|
|||||||
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
||||||
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_time_log(args):
|
||||||
|
if isinstance(args, str):
|
||||||
|
args = json.loads(args)
|
||||||
|
|
||||||
|
args = frappe._dict(args)
|
||||||
|
doc = frappe.get_doc("Job Card", args.job_card_id)
|
||||||
|
doc.validate_sequence_id()
|
||||||
|
doc.add_time_log(args)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_operation_details(work_order, operation):
|
def get_operation_details(work_order, operation):
|
||||||
if work_order and operation:
|
if work_order and operation:
|
||||||
@ -511,3 +674,28 @@ def get_job_details(start, end, filters=None):
|
|||||||
events.append(job_card_data)
|
events.append(job_card_data)
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
|
||||||
|
def set_missing_values(source, target):
|
||||||
|
target.is_corrective_job_card = 1
|
||||||
|
target.operation = operation
|
||||||
|
target.for_operation = for_operation
|
||||||
|
|
||||||
|
target.set('time_logs', [])
|
||||||
|
target.set('employee', [])
|
||||||
|
target.set('items', [])
|
||||||
|
target.get_sub_operations()
|
||||||
|
target.get_required_items()
|
||||||
|
target.validate_time_logs()
|
||||||
|
|
||||||
|
doclist = get_mapped_doc("Job Card", source_name, {
|
||||||
|
"Job Card": {
|
||||||
|
"doctype": "Job Card",
|
||||||
|
"field_map": {
|
||||||
|
"name": "for_job_card",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
return doclist
|
@ -25,8 +25,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"options": "Item",
|
"options": "Item"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "source_warehouse",
|
"fieldname": "source_warehouse",
|
||||||
@ -67,8 +66,7 @@
|
|||||||
"fieldname": "required_qty",
|
"fieldname": "required_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Required Qty",
|
"label": "Required Qty"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_9",
|
"fieldname": "column_break_9",
|
||||||
@ -107,7 +105,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-02-11 13:50:13.804108",
|
"modified": "2021-04-22 18:50:00.003444",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Item",
|
"name": "Job Card Item",
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-12-07 16:58:38.449041",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"sub_operation",
|
||||||
|
"completed_time",
|
||||||
|
"status",
|
||||||
|
"completed_qty"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "Pending",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Complete\nPause\nPending\nWork In Progress",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "In mins",
|
||||||
|
"fieldname": "completed_time",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Completed Time",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sub_operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Operation",
|
||||||
|
"options": "Operation",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "completed_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Completed Qty",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-03-16 18:24:35.399593",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Job Card Operation",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class JobCardOperation(Document):
|
||||||
|
pass
|
@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2019-03-08 23:56:43.187569",
|
"creation": "2019-03-08 23:56:43.187569",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"employee",
|
||||||
"from_time",
|
"from_time",
|
||||||
"to_time",
|
"to_time",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
"completed_qty"
|
"completed_qty",
|
||||||
|
"operation"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -41,10 +44,27 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Completed Qty",
|
"label": "Completed Qty",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "employee",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Employee",
|
||||||
|
"options": "Employee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Operation",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Operation",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-12-03 12:56:02.285448",
|
"links": [],
|
||||||
|
"modified": "2020-12-23 14:30:00.970916",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Time Log",
|
"name": "Job Card Time Log",
|
||||||
|
@ -26,7 +26,10 @@
|
|||||||
"column_break_16",
|
"column_break_16",
|
||||||
"overproduction_percentage_for_work_order",
|
"overproduction_percentage_for_work_order",
|
||||||
"other_settings_section",
|
"other_settings_section",
|
||||||
"update_bom_costs_automatically"
|
"update_bom_costs_automatically",
|
||||||
|
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
|
"column_break_23",
|
||||||
|
"make_serial_no_batch_from_work_order"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -155,13 +158,30 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_5",
|
"fieldname": "column_break_5",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_23",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
|
||||||
|
"fieldname": "make_serial_no_batch_from_work_order",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Make Serial No / Batch from Work Order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Add Corrective Operation Cost in Finished Good Valuation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-13 10:55:43.996581",
|
"modified": "2021-03-16 15:54:38.967341",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
|
@ -2,7 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Operation', {
|
frappe.ui.form.on('Operation', {
|
||||||
refresh: function(frm) {
|
setup: function(frm) {
|
||||||
|
frm.set_query('operation', 'sub_operations', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'name': ['not in', [frm.doc.name]]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -1,167 +1,132 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "Prompt",
|
"autoname": "Prompt",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2014-11-07 16:20:30.683186",
|
"creation": "2014-11-07 16:20:30.683186",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 0,
|
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"workstation",
|
||||||
|
"data_2",
|
||||||
|
"is_corrective_operation",
|
||||||
|
"job_card_section",
|
||||||
|
"create_job_card_based_on_batch_size",
|
||||||
|
"column_break_6",
|
||||||
|
"batch_size",
|
||||||
|
"sub_operations_section",
|
||||||
|
"sub_operations",
|
||||||
|
"total_operation_time",
|
||||||
|
"section_break_4",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "workstation",
|
"fieldname": "workstation",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Default Workstation",
|
"label": "Default Workstation",
|
||||||
"length": 0,
|
"options": "Workstation"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Workstation",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"collapsible": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_4",
|
"fieldname": "section_break_4",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Operation Description"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"hidden": 0,
|
"label": "Description"
|
||||||
"ignore_user_permissions": 0,
|
},
|
||||||
"ignore_xss_filter": 0,
|
{
|
||||||
"in_filter": 0,
|
"collapsible": 1,
|
||||||
"in_list_view": 0,
|
"fieldname": "sub_operations_section",
|
||||||
"in_standard_filter": 0,
|
"fieldtype": "Section Break",
|
||||||
"label": "Description",
|
"label": "Sub Operations"
|
||||||
"length": 0,
|
},
|
||||||
"no_copy": 0,
|
{
|
||||||
"permlevel": 0,
|
"fieldname": "sub_operations",
|
||||||
"precision": "",
|
"fieldtype": "Table",
|
||||||
"print_hide": 0,
|
"options": "Sub Operation"
|
||||||
"print_hide_if_no_value": 0,
|
},
|
||||||
"read_only": 0,
|
{
|
||||||
"remember_last_selected_value": 0,
|
"description": "Time in mins.",
|
||||||
"report_hide": 0,
|
"fieldname": "total_operation_time",
|
||||||
"reqd": 0,
|
"fieldtype": "Float",
|
||||||
"search_index": 0,
|
"label": "Total Operation Time",
|
||||||
"set_only_once": 0,
|
"read_only": 1
|
||||||
"unique": 0
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "data_2",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"depends_on": "create_job_card_based_on_batch_size",
|
||||||
|
"fieldname": "batch_size",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Batch Size",
|
||||||
|
"mandatory_depends_on": "create_job_card_based_on_batch_size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "create_job_card_based_on_batch_size",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Create Job Card based on Batch Size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "job_card_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Job Card"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_6",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_corrective_operation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Corrective Operation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"icon": "fa fa-wrench",
|
"icon": "fa fa-wrench",
|
||||||
"idx": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"image_view": 0,
|
"links": [],
|
||||||
"in_create": 0,
|
"modified": "2021-01-12 15:09:23.593338",
|
||||||
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2016-11-07 05:28:27.462413",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Operation",
|
"name": "Operation",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 0,
|
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 1,
|
"import": 1,
|
||||||
"is_custom": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "Manufacturing User",
|
"role": "Manufacturing User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 0,
|
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 1,
|
"import": 1,
|
||||||
"is_custom": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Manufacturing Manager",
|
"role": "Manufacturing Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_seen": 0
|
"track_changes": 1
|
||||||
}
|
}
|
@ -2,9 +2,34 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class Operation(Document):
|
class Operation(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if not self.description:
|
if not self.description:
|
||||||
self.description = self.name
|
self.description = self.name
|
||||||
|
|
||||||
|
self.duplicate_sub_operation()
|
||||||
|
self.set_total_time()
|
||||||
|
|
||||||
|
def duplicate_sub_operation(self):
|
||||||
|
operation_list = []
|
||||||
|
for row in self.sub_operations:
|
||||||
|
if row.operation in operation_list:
|
||||||
|
frappe.throw(_("The operation {0} can not add multiple times")
|
||||||
|
.format(frappe.bold(row.operation)))
|
||||||
|
|
||||||
|
if self.name == row.operation:
|
||||||
|
frappe.throw(_("The operation {0} can not be the sub operation")
|
||||||
|
.format(frappe.bold(row.operation)))
|
||||||
|
|
||||||
|
operation_list.append(row.operation)
|
||||||
|
|
||||||
|
def set_total_time(self):
|
||||||
|
self.total_operation_time = 0.0
|
||||||
|
|
||||||
|
for row in self.sub_operations:
|
||||||
|
self.total_operation_time += row.time_in_mins
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on('Production Plan', {
|
frappe.ui.form.on('Production Plan', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
'Work Order': 'Work Order',
|
'Work Order': 'Work Order / Subcontract PO',
|
||||||
'Material Request': 'Material Request',
|
'Material Request': 'Material Request',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
frm.trigger("show_progress");
|
frm.trigger("show_progress");
|
||||||
|
|
||||||
if (frm.doc.status !== "Completed") {
|
if (frm.doc.status !== "Completed") {
|
||||||
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
frm.add_custom_button(__("Work Order Tree"), ()=> {
|
||||||
frm.add_custom_button(__("Work Order"), ()=> {
|
frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
|
||||||
frm.trigger("make_work_order");
|
}, __('View'));
|
||||||
}, __('Create'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
frm.add_custom_button(__("Production Plan Summary"), ()=> {
|
||||||
frm.add_custom_button(__("Material Request"), ()=> {
|
frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
|
||||||
frm.trigger("make_material_request");
|
}, __('View'));
|
||||||
}, __('Create'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.status === "Closed") {
|
if (frm.doc.status === "Closed") {
|
||||||
frm.add_custom_button(__("Re-open"), function() {
|
frm.add_custom_button(__("Re-open"), function() {
|
||||||
@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
frm.events.close_open_production_plan(frm, true);
|
frm.events.close_open_production_plan(frm, true);
|
||||||
}, __("Status"));
|
}, __("Status"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
||||||
|
frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
|
||||||
|
frm.trigger("make_work_order");
|
||||||
|
}, __('Create'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
||||||
|
frm.add_custom_button(__("Material Request"), ()=> {
|
||||||
|
frm.trigger("make_material_request");
|
||||||
|
}, __('Create'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_sub_assembly_items: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "get_sub_assembly_items",
|
||||||
|
freeze: true,
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function() {
|
||||||
|
refresh_field("sub_assembly_items");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
get_items_for_mr: function(frm) {
|
get_items_for_mr: function(frm) {
|
||||||
if (!frm.doc.for_warehouse) {
|
if (!frm.doc.for_warehouse) {
|
||||||
frappe.throw(__("Select warehouse for material requests"));
|
frappe.throw(__("Select warehouse for material requests"));
|
||||||
@ -306,8 +325,25 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
download_materials_required: function(frm) {
|
download_materials_required: function(frm) {
|
||||||
|
const fields = [{
|
||||||
|
fieldname: 'warehouses',
|
||||||
|
fieldtype: 'Table MultiSelect',
|
||||||
|
label: __('Warehouses'),
|
||||||
|
default: frm.doc.from_warehouse,
|
||||||
|
options: "Production Plan Material Request Warehouse",
|
||||||
|
get_query: function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
frappe.prompt(fields, (row) => {
|
||||||
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
|
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
|
||||||
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc });
|
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses });
|
||||||
|
}, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock'));
|
||||||
},
|
},
|
||||||
|
|
||||||
show_progress: function(frm) {
|
show_progress: function(frm) {
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
"po_items",
|
"po_items",
|
||||||
"section_break_25",
|
"section_break_25",
|
||||||
"prod_plan_references",
|
"prod_plan_references",
|
||||||
|
"section_break_24",
|
||||||
|
"get_sub_assembly_items",
|
||||||
|
"sub_assembly_items",
|
||||||
"material_request_planning",
|
"material_request_planning",
|
||||||
"include_non_stock_items",
|
"include_non_stock_items",
|
||||||
"include_subcontracted_items",
|
"include_subcontracted_items",
|
||||||
@ -187,7 +190,7 @@
|
|||||||
"depends_on": "get_items_from",
|
"depends_on": "get_items_from",
|
||||||
"fieldname": "get_items",
|
"fieldname": "get_items",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Get Items For Work Order"
|
"label": "Get Finished Goods for Manufacture"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "po_items",
|
"fieldname": "po_items",
|
||||||
@ -199,7 +202,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "material_request_planning",
|
"fieldname": "material_request_planning",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Material Request Planning"
|
"label": "Material Requirement Planning"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
@ -237,12 +240,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_27",
|
"fieldname": "section_break_27",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "mr_items",
|
"fieldname": "mr_items",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Material Request Plan Item",
|
"label": "Raw Materials",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Material Request Plan Item"
|
"options": "Material Request Plan Item"
|
||||||
},
|
},
|
||||||
@ -337,13 +341,30 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Production Plan Item Reference",
|
"label": "Production Plan Item Reference",
|
||||||
"options": "Production Plan Item Reference"
|
"options": "Production Plan Item Reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_24",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sub_assembly_items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Sub Assembly Items",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Production Plan Sub Assembly Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_sub_assembly_items",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Sub Assembly Items"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-24 16:59:03.643211",
|
"modified": "2021-06-28 20:00:33.905114",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
@ -5,10 +5,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, json, copy
|
import frappe, json, copy
|
||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from six import string_types, iteritems
|
from six import iteritems
|
||||||
|
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
|
from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
|
||||||
|
ceil, get_link_to_form, getdate)
|
||||||
from frappe.utils.csvutils import build_csv_response
|
from frappe.utils.csvutils import build_csv_response
|
||||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
|
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||||
@ -349,50 +350,89 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_work_order(self):
|
def make_work_order(self):
|
||||||
wo_list = []
|
wo_list, po_list = [], []
|
||||||
|
subcontracted_po = {}
|
||||||
|
|
||||||
self.validate_data()
|
self.validate_data()
|
||||||
|
self.make_work_order_for_finished_goods(wo_list)
|
||||||
|
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
|
||||||
|
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
|
||||||
|
self.show_list_created_message('Work Order', wo_list)
|
||||||
|
self.show_list_created_message('Purchase Order', po_list)
|
||||||
|
|
||||||
|
def make_work_order_for_finished_goods(self, wo_list):
|
||||||
items_data = self.get_production_items()
|
items_data = self.get_production_items()
|
||||||
|
|
||||||
for key, item in items_data.items():
|
for key, item in items_data.items():
|
||||||
|
if self.sub_assembly_items:
|
||||||
|
item['use_multi_level_bom'] = 0
|
||||||
|
|
||||||
work_order = self.create_work_order(item)
|
work_order = self.create_work_order(item)
|
||||||
if work_order:
|
if work_order:
|
||||||
wo_list.append(work_order)
|
wo_list.append(work_order)
|
||||||
|
|
||||||
if item.get("make_work_order_for_sub_assembly_items"):
|
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
|
||||||
work_orders = self.make_work_order_for_sub_assembly_items(item)
|
for row in self.sub_assembly_items:
|
||||||
wo_list.extend(work_orders)
|
if row.type_of_manufacturing == 'Subcontract':
|
||||||
|
subcontracted_po.setdefault(row.supplier, []).append(row)
|
||||||
|
continue
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
self.prepare_args_for_sub_assembly_items(row, args)
|
||||||
|
work_order = self.create_work_order(args)
|
||||||
|
if work_order:
|
||||||
|
wo_list.append(work_order)
|
||||||
|
|
||||||
|
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
|
||||||
|
if not subcontracted_po:
|
||||||
|
return
|
||||||
|
|
||||||
|
for supplier, po_list in subcontracted_po.items():
|
||||||
|
po = frappe.new_doc('Purchase Order')
|
||||||
|
po.supplier = supplier
|
||||||
|
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
|
||||||
|
po.is_subcontracted_item = 'Yes'
|
||||||
|
for row in po_list:
|
||||||
|
args = {
|
||||||
|
'item_code': row.production_item,
|
||||||
|
'warehouse': row.fg_warehouse,
|
||||||
|
'production_plan_sub_assembly_item': row.name,
|
||||||
|
'bom': row.bom_no,
|
||||||
|
'production_plan': self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
|
||||||
|
'description', 'production_plan_item']:
|
||||||
|
args[field] = row.get(field)
|
||||||
|
|
||||||
|
po.append('items', args)
|
||||||
|
|
||||||
|
po.set_missing_values()
|
||||||
|
po.flags.ignore_mandatory = True
|
||||||
|
po.flags.ignore_validate = True
|
||||||
|
po.insert()
|
||||||
|
purchase_orders.append(po.name)
|
||||||
|
|
||||||
|
def show_list_created_message(self, doctype, doc_list=None):
|
||||||
|
if not doc_list:
|
||||||
|
return
|
||||||
|
|
||||||
frappe.flags.mute_messages = False
|
frappe.flags.mute_messages = False
|
||||||
|
if doc_list:
|
||||||
|
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
|
||||||
|
msgprint(_("{0} created").format(comma_and(doc_list)))
|
||||||
|
|
||||||
if wo_list:
|
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||||
wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
|
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||||
(p, p) for p in wo_list]
|
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
|
||||||
msgprint(_("{0} created").format(comma_and(wo_list)))
|
args[field] = row.get(field)
|
||||||
else :
|
|
||||||
msgprint(_("No Work Orders created"))
|
|
||||||
|
|
||||||
def make_work_order_for_sub_assembly_items(self, item):
|
args.update({
|
||||||
work_orders = []
|
"use_multi_level_bom": 0,
|
||||||
bom_data = {}
|
"production_plan": self.name,
|
||||||
|
"production_plan_sub_assembly_item": row.name
|
||||||
get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
|
|
||||||
|
|
||||||
for key, data in bom_data.items():
|
|
||||||
data.update({
|
|
||||||
'qty': data.get("stock_qty"),
|
|
||||||
'production_plan': self.name,
|
|
||||||
'use_multi_level_bom': item.get("use_multi_level_bom"),
|
|
||||||
'company': self.company,
|
|
||||||
'fg_warehouse': item.get("fg_warehouse"),
|
|
||||||
'update_consumed_material_cost_in_project': 0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
work_order = self.create_work_order(data)
|
|
||||||
if work_order:
|
|
||||||
work_orders.append(work_order)
|
|
||||||
|
|
||||||
return work_orders
|
|
||||||
|
|
||||||
def create_work_order(self, item):
|
def create_work_order(self, item):
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
|
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
|
||||||
warehouse = get_default_warehouse()
|
warehouse = get_default_warehouse()
|
||||||
@ -477,18 +517,42 @@ class ProductionPlan(Document):
|
|||||||
msgprint(_("No material request created"))
|
msgprint(_("No material request created"))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def download_raw_materials(doc):
|
def get_sub_assembly_items(self, manufacturing_type=None):
|
||||||
if isinstance(doc, string_types):
|
self.sub_assembly_items = []
|
||||||
|
for row in self.po_items:
|
||||||
|
bom_data = []
|
||||||
|
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||||
|
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||||
|
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
||||||
|
|
||||||
|
for data in bom_data:
|
||||||
|
data.qty = data.stock_qty
|
||||||
|
data.production_plan_item = row.name
|
||||||
|
data.fg_warehouse = row.warehouse
|
||||||
|
data.schedule_date = row.planned_start_date
|
||||||
|
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
|
||||||
|
else "In House")
|
||||||
|
|
||||||
|
self.append("sub_assembly_items", data)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def download_raw_materials(doc, warehouses=None):
|
||||||
|
if isinstance(doc, str):
|
||||||
doc = frappe._dict(json.loads(doc))
|
doc = frappe._dict(json.loads(doc))
|
||||||
|
|
||||||
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
|
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
|
||||||
'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
|
'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
|
||||||
'Safety Stock', 'Required Qty']]
|
'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
|
||||||
|
|
||||||
for d in get_items_for_material_requests(doc):
|
doc.warehouse = None
|
||||||
|
for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True):
|
||||||
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
|
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
|
||||||
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
|
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
|
||||||
d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
|
d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
|
||||||
|
|
||||||
if not doc.get('for_warehouse'):
|
if not doc.get('for_warehouse'):
|
||||||
row = {'item_code': d.get('item_code')}
|
row = {'item_code': d.get('item_code')}
|
||||||
@ -507,7 +571,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
|||||||
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
|
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
|
||||||
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
|
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
|
||||||
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
|
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
|
||||||
item.purchase_uom, item_uom.conversion_factor
|
item.purchase_uom, item_uom.conversion_factor, item.safety_stock
|
||||||
from
|
from
|
||||||
`tabBOM Explosion Item` bei
|
`tabBOM Explosion Item` bei
|
||||||
JOIN `tabBOM` bom ON bom.name = bei.parent
|
JOIN `tabBOM` bom ON bom.name = bei.parent
|
||||||
@ -659,7 +723,7 @@ def get_sales_orders(self):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
|
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
|
||||||
if isinstance(row, string_types):
|
if isinstance(row, str):
|
||||||
row = frappe._dict(json.loads(row))
|
row = frappe._dict(json.loads(row))
|
||||||
|
|
||||||
company = frappe.db.escape(company)
|
company = frappe.db.escape(company)
|
||||||
@ -677,19 +741,17 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
|
|||||||
|
|
||||||
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
|
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
|
||||||
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
|
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
|
||||||
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin`
|
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
|
||||||
where item_code = %(item_code)s {conditions}
|
ifnull(sum(planned_qty),0) as planned_qty
|
||||||
|
from `tabBin` where item_code = %(item_code)s {conditions}
|
||||||
group by item_code, warehouse
|
group by item_code, warehouse
|
||||||
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
|
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
|
||||||
|
|
||||||
@frappe.whitelist()
|
def get_warehouse_list(warehouses, warehouse_list=None):
|
||||||
def get_items_for_material_requests(doc, warehouses=None):
|
if not warehouse_list:
|
||||||
if isinstance(doc, string_types):
|
|
||||||
doc = frappe._dict(json.loads(doc))
|
|
||||||
|
|
||||||
warehouse_list = []
|
warehouse_list = []
|
||||||
if warehouses:
|
|
||||||
if isinstance(warehouses, string_types):
|
if isinstance(warehouses, str):
|
||||||
warehouses = json.loads(warehouses)
|
warehouses = json.loads(warehouses)
|
||||||
|
|
||||||
for row in warehouses:
|
for row in warehouses:
|
||||||
@ -699,10 +761,19 @@ def get_items_for_material_requests(doc, warehouses=None):
|
|||||||
else:
|
else:
|
||||||
warehouse_list.append(row.get("warehouse"))
|
warehouse_list.append(row.get("warehouse"))
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = frappe._dict(json.loads(doc))
|
||||||
|
|
||||||
|
warehouse_list = []
|
||||||
|
if warehouses:
|
||||||
|
get_warehouse_list(warehouses, warehouse_list)
|
||||||
|
|
||||||
if warehouse_list:
|
if warehouse_list:
|
||||||
warehouses = list(set(warehouse_list))
|
warehouses = list(set(warehouse_list))
|
||||||
|
|
||||||
if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses:
|
if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
|
||||||
warehouses.remove(doc.get("for_warehouse"))
|
warehouses.remove(doc.get("for_warehouse"))
|
||||||
|
|
||||||
warehouse_list = None
|
warehouse_list = None
|
||||||
@ -721,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None):
|
|||||||
|
|
||||||
so_item_details = frappe._dict()
|
so_item_details = frappe._dict()
|
||||||
for data in po_items:
|
for data in po_items:
|
||||||
|
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||||
|
data["include_exploded_items"] = 1
|
||||||
|
|
||||||
planned_qty = data.get('required_qty') or data.get('planned_qty')
|
planned_qty = data.get('required_qty') or data.get('planned_qty')
|
||||||
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
|
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
|
||||||
warehouse = doc.get('for_warehouse')
|
warehouse = doc.get('for_warehouse')
|
||||||
@ -795,7 +869,7 @@ def get_items_for_material_requests(doc, warehouses=None):
|
|||||||
if items:
|
if items:
|
||||||
mr_items.append(items)
|
mr_items.append(items)
|
||||||
|
|
||||||
if not ignore_existing_ordered_qty and warehouses:
|
if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
|
||||||
new_mr_items = []
|
new_mr_items = []
|
||||||
for item in mr_items:
|
for item in mr_items:
|
||||||
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
|
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
|
||||||
@ -852,23 +926,28 @@ def get_item_data(item_code):
|
|||||||
# "description": item_details.get("description")
|
# "description": item_details.get("description")
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
|
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||||
data = get_children('BOM', parent = bom_no)
|
data = get_children('BOM', parent = bom_no)
|
||||||
for d in data:
|
for d in data:
|
||||||
if d.expandable:
|
if d.expandable:
|
||||||
key = (d.name, d.value)
|
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||||
if key not in bom_data:
|
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
|
||||||
bom_data.setdefault(key, {
|
if d.value else 0)
|
||||||
'stock_qty': 0,
|
|
||||||
|
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||||
|
bom_data.append(frappe._dict({
|
||||||
|
'parent_item_code': parent_item_code,
|
||||||
'description': d.description,
|
'description': d.description,
|
||||||
'production_item': d.item_code,
|
'production_item': d.item_code,
|
||||||
'item_name': d.item_name,
|
'item_name': d.item_name,
|
||||||
'stock_uom': d.stock_uom,
|
'stock_uom': d.stock_uom,
|
||||||
'uom': d.stock_uom,
|
'uom': d.stock_uom,
|
||||||
'bom_no': d.value
|
'bom_no': d.value,
|
||||||
})
|
'is_sub_contracted_item': d.is_sub_contracted_item,
|
||||||
|
'bom_level': bom_level,
|
||||||
|
'indent': indent,
|
||||||
|
'stock_qty': stock_qty
|
||||||
|
}))
|
||||||
|
|
||||||
bom_item = bom_data.get(key)
|
if d.value:
|
||||||
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
|
||||||
|
|
||||||
get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
|
|
||||||
|
@ -9,5 +9,9 @@ def get_data():
|
|||||||
'label': _('Transactions'),
|
'label': _('Transactions'),
|
||||||
'items': ['Work Order', 'Material Request']
|
'items': ['Work Order', 'Material Request']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'label': _('Subcontract'),
|
||||||
|
'items': ['Purchase Order']
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase):
|
|||||||
pln.append("po_items", {
|
pln.append("po_items", {
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
|
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
|
||||||
"planned_qty": 3,
|
"planned_qty": 3
|
||||||
"make_work_order_for_sub_assembly_items": 1
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pln.get_sub_assembly_items('In House')
|
||||||
pln.submit()
|
pln.submit()
|
||||||
pln.make_work_order()
|
pln.make_work_order()
|
||||||
|
|
||||||
|
@ -9,18 +9,17 @@
|
|||||||
"include_exploded_items",
|
"include_exploded_items",
|
||||||
"item_code",
|
"item_code",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
"planned_qty",
|
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
"make_work_order_for_sub_assembly_items",
|
"planned_qty",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"planned_start_date",
|
"planned_start_date",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"pending_qty",
|
"pending_qty",
|
||||||
"ordered_qty",
|
"ordered_qty",
|
||||||
"produced_qty",
|
|
||||||
"column_break_17",
|
"column_break_17",
|
||||||
"description",
|
"description",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
|
"produced_qty",
|
||||||
"reference_section",
|
"reference_section",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"sales_order_item",
|
"sales_order_item",
|
||||||
@ -32,11 +31,10 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 1,
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "include_exploded_items",
|
"fieldname": "include_exploded_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Include Exploded Items"
|
"label": "Include Exploded Items"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,13 +78,6 @@
|
|||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
|
|
||||||
"fieldname": "make_work_order_for_sub_assembly_items",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Make Work Order for Sub Assembly Items"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -218,7 +209,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-28 19:14:57.772123",
|
"modified": "2021-06-28 18:31:06.822168",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Item",
|
"name": "Production Plan Item",
|
||||||
|
@ -0,0 +1,202 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-12-27 16:08:36.127199",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"production_item",
|
||||||
|
"item_name",
|
||||||
|
"fg_warehouse",
|
||||||
|
"parent_item_code",
|
||||||
|
"schedule_date",
|
||||||
|
"column_break_3",
|
||||||
|
"qty",
|
||||||
|
"bom_no",
|
||||||
|
"bom_level",
|
||||||
|
"type_of_manufacturing",
|
||||||
|
"supplier",
|
||||||
|
"work_order_details_section",
|
||||||
|
"work_order",
|
||||||
|
"purchase_order",
|
||||||
|
"production_plan_item",
|
||||||
|
"column_break_7",
|
||||||
|
"produced_qty",
|
||||||
|
"received_qty",
|
||||||
|
"indent",
|
||||||
|
"section_break_19",
|
||||||
|
"uom",
|
||||||
|
"stock_uom",
|
||||||
|
"column_break_22",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fetch_from": "sub_assembly_item_code.item_name",
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
|
||||||
|
"fieldname": "work_order_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "work_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Work Order",
|
||||||
|
"options": "Work Order",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Required Qty",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Purchase Order",
|
||||||
|
"options": "Purchase Order",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "received_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Received Qty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "bom_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Bom No",
|
||||||
|
"options": "BOM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_plan_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Production Plan Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "parent_item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Finished Good",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fetch_from": "bom_no.bom_level",
|
||||||
|
"fieldname": "bom_level",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Level (BOM)",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_19",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Stock UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_22",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "description",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_item",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Sub Assembly Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "indent",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Indent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fg_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Target Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "produced_qty",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Produced Quantity",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "In House",
|
||||||
|
"fieldname": "type_of_manufacturing",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Manufacturing Type",
|
||||||
|
"options": "In House\nSubcontract"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "supplier",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Supplier",
|
||||||
|
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
|
||||||
|
"options": "Supplier"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "schedule_date",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Schedule Date"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-28 20:10:56.296410",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Production Plan Sub Assembly Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class ProductionPlanSubAssemblyItem(Document):
|
||||||
|
pass
|
@ -4,14 +4,24 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, flt
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class Routing(Document):
|
class Routing(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.calculate_operating_cost()
|
||||||
self.set_routing_id()
|
self.set_routing_id()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
self.calculate_operating_cost()
|
||||||
|
|
||||||
|
def calculate_operating_cost(self):
|
||||||
|
for operation in self.operations:
|
||||||
|
if not operation.hour_rate:
|
||||||
|
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
|
||||||
|
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
|
||||||
|
|
||||||
def set_routing_id(self):
|
def set_routing_id(self):
|
||||||
sequence_id = 0
|
sequence_id = 0
|
||||||
for row in self.operations:
|
for row in self.operations:
|
||||||
|
@ -7,9 +7,7 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
|
||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
|
||||||
class TestRouting(unittest.TestCase):
|
class TestRouting(unittest.TestCase):
|
||||||
@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
|
|||||||
wo_doc.cancel()
|
wo_doc.cancel()
|
||||||
wo_doc.delete()
|
wo_doc.delete()
|
||||||
|
|
||||||
|
def test_update_bom_operation_time(self):
|
||||||
|
operations = [
|
||||||
|
{
|
||||||
|
"operation": "Test Operation A",
|
||||||
|
"workstation": "_Test Workstation A",
|
||||||
|
"hour_rate_rent": 300,
|
||||||
|
"hour_rate_labour": 750 ,
|
||||||
|
"time_in_mins": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "Test Operation B",
|
||||||
|
"workstation": "_Test Workstation B",
|
||||||
|
"hour_rate_labour": 200,
|
||||||
|
"hour_rate_rent": 1000,
|
||||||
|
"time_in_mins": 20
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
test_routing_operations = [
|
||||||
|
{
|
||||||
|
"operation": "Test Operation A",
|
||||||
|
"workstation": "_Test Workstation A",
|
||||||
|
"time_in_mins": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "Test Operation B",
|
||||||
|
"workstation": "_Test Workstation A",
|
||||||
|
"time_in_mins": 20
|
||||||
|
}
|
||||||
|
]
|
||||||
|
setup_operations(operations)
|
||||||
|
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
|
||||||
|
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
|
||||||
|
self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
|
||||||
|
self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
|
||||||
|
routing_doc.operations[0].time_in_mins = 90
|
||||||
|
routing_doc.operations[1].time_in_mins = 42.2
|
||||||
|
routing_doc.save()
|
||||||
|
bom_doc.update_cost()
|
||||||
|
bom_doc.reload()
|
||||||
|
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
||||||
|
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
||||||
|
|
||||||
|
|
||||||
def setup_operations(rows):
|
def setup_operations(rows):
|
||||||
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
|
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||||
for row in rows:
|
for row in rows:
|
||||||
make_workstation(row)
|
make_workstation(row)
|
||||||
make_operation(row)
|
make_operation(row)
|
||||||
@ -61,12 +105,14 @@ def create_routing(**args):
|
|||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
try:
|
try:
|
||||||
for operation in args.operations:
|
|
||||||
doc.append("operations", operation)
|
|
||||||
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
doc = frappe.get_doc("Routing", args.routing_name)
|
doc = frappe.get_doc("Routing", args.routing_name)
|
||||||
|
doc.delete_key('operations')
|
||||||
|
for operation in args.operations:
|
||||||
|
doc.append("operations", operation)
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@ -91,7 +137,7 @@ def setup_bom(**args):
|
|||||||
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
|
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
|
||||||
if not name:
|
if not name:
|
||||||
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
|
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
|
||||||
routing = args.routing, with_operations=1)
|
routing = args.routing, with_operations=1, currency = args.currency)
|
||||||
else:
|
else:
|
||||||
bom_doc = frappe.get_doc("BOM", name)
|
bom_doc = frappe.get_doc("BOM", name)
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Sub Operation', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-12-07 15:39:47.488519",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"operation",
|
||||||
|
"time_in_mins",
|
||||||
|
"column_break_5",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Operation",
|
||||||
|
"options": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Time in mins",
|
||||||
|
"fieldname": "time_in_mins",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Operation Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_5",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-12-07 18:09:18.005578",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Sub Operation",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
10
erpnext/manufacturing/doctype/sub_operation/sub_operation.py
Normal file
10
erpnext/manufacturing/doctype/sub_operation/sub_operation.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SubOperation(Document):
|
||||||
|
pass
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user