Merge branch 'develop' into patch-4

This commit is contained in:
Deepesh Garg
2021-07-01 18:08:22 +05:30
committed by GitHub
165 changed files with 4516 additions and 1108 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.5.1' __version__ = '13.6.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -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 {}

View File

@@ -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)

View File

@@ -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)

View File

@@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={ dunnings = frappe.get_list('Dunning', filters={
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
@@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte
grand_total = 0 grand_total = 0
if rate_of_interest: if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365 interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
dunning_amount = flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee)
return { return {

View File

@@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))

View File

@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc, doc: frm.doc,
btn: $(btn_primary), btn: $(btn_primary),
method: "make_invoices", method: "make_invoices",
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]) freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
callback: function(r) {
if (r.message.length == 1) {
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
} else if (r.message.length < 50) {
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
}
}
}); });
}); });

View File

@@ -216,7 +216,8 @@ def start_import(invoices):
return names return names
def publish(index, total, doctype): def publish(index, total, doctype):
if total < 5: return if total < 50:
return
frappe.publish_realtime( frappe.publish_realtime(
"opening_invoice_creation_progress", "opening_invoice_creation_progress",
dict( dict(
@@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
return accounts[0].name return accounts[0].name

View File

@@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);

View File

@@ -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",

View File

@@ -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):

View File

@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}); });
} }
company() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
}
onload() { onload() {
super.onload(); super.onload();
@@ -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);
},
}) })

View File

@@ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController):
if d.category in ('Valuation', 'Total and Valuation') if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)] and flt(d.base_tax_amount_after_discount_amount)]
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount): if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account)
@@ -634,6 +636,34 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project "project": item.project or self.project
}, account_currency, item=item)) }, account_currency, item=item))
# check if the exchange rate has changed
if item.get('purchase_receipt'):
if exchange_rate_map[item.purchase_receipt] and \
self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \
item.net_rate == net_rate_map[item.pr_detail]:
discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \
(exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
gl_entries.append(
self.get_gl_dict({
"account": expense_account,
"against": self.supplier,
"debit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
gl_entries.append(
self.get_gl_dict({
"account": self.get_company_default("exchange_gain_loss_account"),
"against": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
# If asset is bought through this document and not linked to PR # If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount: if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1141,6 +1171,36 @@ class PurchaseInvoice(BuyingController):
if update: if update:
self.db_set('status', self.status, update_modified = update_modified) self.db_set('status', self.status, update_modified = update_modified)
# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling
def get_purchase_document_details(doc):
if doc.doctype == 'Purchase Invoice':
doc_reference = 'purchase_receipt'
items_reference = 'pr_detail'
parent_doctype = 'Purchase Receipt'
child_doctype = 'Purchase Receipt Item'
else:
doc_reference = 'purchase_invoice'
items_reference = 'purchase_invoice_item'
parent_doctype = 'Purchase Invoice'
child_doctype = 'Purchase Invoice Item'
purchase_receipts_or_invoices = []
items = []
for item in doc.get('items'):
if item.get(doc_reference):
purchase_receipts_or_invoices.append(item.get(doc_reference))
if item.get(items_reference):
items.append(item.get(items_reference))
exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))
net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in',
items)}, fields=['name', 'net_rate'], as_list=1))
return exchange_rate_map, net_rate_map
def get_list_context(context=None): def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context) list_context = get_list_context(context)

View File

@@ -230,6 +230,27 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_exchange_rate_difference(self):
pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
pi.items[0].purchase_receipt = pr.name
pi.items[0].pr_detail = pr.items[0].name
pi.insert()
pi.submit()
# fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
self.assertEqual(pi.name, voucher_no)
exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
def test_purchase_invoice_change_naming_series(self): def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1]) pi = frappe.copy_doc(test_records[1])
pi.insert() pi.insert()
@@ -966,7 +987,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 +1023,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()

View File

@@ -854,7 +854,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-16 19:33:51.099386", "modified": "2021-06-16 19:43:51.099386",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -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,33 +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'):
address = frappe.get_doc({ address = frappe.get_doc({

View File

@@ -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

View File

@@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{ {
"fieldname":"account", "fieldname":"account",
"label": __("Account"), "label": __("Account"),
"fieldtype": "Link", "fieldtype": "MultiSelectList",
"options": "Account", "options": "Account",
"get_query": function() { get_data: function(txt) {
var company = frappe.query_report.get_filter_value('company'); return frappe.db.get_link_options('Account', txt, {
return { company: frappe.query_report.get_filter_value("company")
"doctype": "Account", });
"filters": {
"company": company,
}
}
} }
}, },
{ {
@@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"), "label": __("Cost Center"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { get_data: function(txt) {
return frappe.db.get_link_options('Cost Center', txt); return frappe.db.get_link_options('Cost Center', txt, {
company: frappe.query_report.get_filter_value("company")
});
} }
}, },
{ {
@@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Project"), "label": __("Project"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { get_data: function(txt) {
return frappe.db.get_link_options('Project', txt); return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
} }
}, },
{ {

View File

@@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"): if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
if filters.get("account") and not account_details.get(filters.account): for account in filters.account:
frappe.throw(_("Account {0} does not exists").format(filters.account)) if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account'))
if (filters.get("account") and filters.get("group_by") == _('Group by Account') if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0): and account_details[filters.account].is_group == 0):
@@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None account_currency = None
if filters.get("account"): if filters.get("account"):
account_currency = get_account_currency(filters.account) if len(filters.get("account")) == 1:
account_currency = get_account_currency(filters.account[0])
else:
currency = get_account_currency(filters.account[0])
is_same_account_currency = True
for account in filters.get("account"):
if get_account_currency(account) != currency:
is_same_account_currency = False
break
if is_same_account_currency:
account_currency = currency
elif filters.get("party"): elif filters.get("party"):
gle_currency = frappe.db.get_value( gle_currency = frappe.db.get_value(
"GL Entry", { "GL Entry", {
@@ -205,10 +221,10 @@ 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"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) if filters.get("account"):
conditions.append("""account in (select name from tabAccount filters.account = get_accounts_with_children(filters.account)
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) conditions.append("account in %(account)s")
if filters.get("cost_center"): if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center) filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@@ -266,6 +282,20 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_accounts_with_children(accounts):
if not isinstance(accounts, list):
accounts = [d.strip() for d in accounts.strip().split(',') if d]
all_accounts = []
for d in accounts:
if frappe.db.exists("Account", d):
lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_accounts += [c.name for c in children]
else:
frappe.throw(_("Account: {0} does not exist").format(d))
return list(set(all_accounts))
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] data = []

View File

@@ -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
} }
] ]

View File

@@ -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",

View File

@@ -0,0 +1,72 @@
# Version 13.6.0 Release Notes
### Features & Enhancements
- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
### Fixes
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))

View File

@@ -828,8 +828,14 @@ 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():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") if self.doctype != "Purchase Invoice":
.format(item.item_code, item.idx, max_allowed_amt)) 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")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname): def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default

View File

@@ -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,

View File

@@ -99,9 +99,10 @@ 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
@@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc):
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no)) serial_nos.extend(get_serial_nos(row.serial_no))
return serial_nos return serial_nos

View File

@@ -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():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) if d.doctype == "Packed Item":
if d.rate != rate: incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
d.rate = rate if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if 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")

View File

@@ -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
@@ -497,9 +497,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):

View File

@@ -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;
} }
} }
} }

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import json import json
from frappe.utils import getdate from frappe.utils import getdate, strip_html
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase): class TestPatientHistorySettings(unittest.TestCase):
@@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase):
self.assertTrue(medical_rec) self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format( expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
frappe.utils.format_date(getdate())) frappe.utils.format_date(getdate()))
self.assertEqual(medical_rec.subject, expected_subject) self.assertEqual(strip_html(medical_rec.subject), expected_subject)
self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate()) self.assertEqual(medical_rec.communication_date, getdate())
@@ -101,4 +101,4 @@ def create_doc(patient):
}).insert() }).insert()
doc.submit() doc.submit()
return doc return doc

View File

@@ -158,6 +158,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 = [

View File

@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{ return{
query: "erpnext.controllers.queries.employee_query" query: "erpnext.controllers.queries.employee_query"
} }
} }

View File

@@ -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

View File

@@ -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);

View File

@@ -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",

View File

@@ -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()

View File

@@ -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'),

View File

@@ -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;
} }
}; };

View 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
};
});
},
});

View 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
}

View 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"))
)

View File

@@ -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];
}
};

View File

@@ -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

View 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) {
// }
});

View 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"
}

View 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

View 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

View File

@@ -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];

View File

@@ -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",
@@ -277,4 +278,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "employee" "timeline_field": "employee"
} }

View File

@@ -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,
@@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type): def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))

View File

@@ -1,10 +1,10 @@
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
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase): class TestLeaveAllocation(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -164,6 +164,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)

View File

@@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) {
}) })
frm.set_value('total_estimated_budget', estimated_budget); frm.set_value('total_estimated_budget', estimated_budget);
} }
} };

View File

@@ -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):
@@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date) designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None

View File

@@ -11,8 +11,8 @@
"event": "Submit", "event": "Submit",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>", "message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>\n {{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b></li>\n {% endif %}\n <li>{{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>{{ _(\"Note: This Training Event is mandatory\") }}</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
"modified": "2021-05-24 16:29:13.165930", "modified": "2021-06-16 14:08:12.933367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Scheduled", "name": "Training Scheduled",

View File

@@ -24,19 +24,19 @@
{% set start = frappe.utils.get_datetime(doc.start_time) %} {% set start = frappe.utils.get_datetime(doc.start_time) %}
{% set end = frappe.utils.get_datetime(doc.end_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %}
{% if start.date() == end.date() %} {% if start.date() == end.date() %}
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li> <li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li> <li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b> {{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li> </li>
{% else %} {% else %}
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b> <li>
</li> {{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b> </li>
</li> <li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% endif %} {% endif %}
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li> <li>{{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
{% if doc.is_mandatory %} {% if doc.is_mandatory %}
<li>Note: This Training Event is mandatory</li> <li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@@ -44,4 +44,4 @@
<td width="15"></td> <td width="15"></td>
</tr> </tr>
<tr height="10"></tr> <tr height="10"></tr>
</table> </table>

View File

@@ -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))

View File

@@ -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",

View File

@@ -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);
}); });

View File

@@ -193,6 +193,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 +236,7 @@
{ {
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@@ -245,6 +247,7 @@
"options": "Routing" "options": "Routing"
}, },
{ {
"depends_on": "with_operations",
"fieldname": "operations", "fieldname": "operations",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Operations", "label": "Operations",
@@ -517,7 +520,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-03-16 12:25:09.081968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -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,7 @@ 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)
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 +224,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 +285,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 +314,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 +475,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 +662,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 +672,11 @@ 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 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)
@@ -975,7 +1067,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
return frappe.get_all("Item", return frappe.get_all("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by, or_filters = or_cond_filters, order_by=order_by,

View File

@@ -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:

View File

@@ -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",

View File

@@ -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);
} else { }, __("Assign Job to Employee"));
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
} else { } else {
frm.events.start_job(frm); frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
} }
}).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', '');
} }
}) })

View File

@@ -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",

View File

@@ -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,100 @@ 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"):
for name in employees:
self.append("time_logs", {
"from_time": get_datetime(args.get("start_time")),
"employee": name.get('employee'),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
})
if not self.employee:
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 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 +291,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 +311,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 +336,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 +350,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 +387,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 +410,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 +494,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 +520,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 +666,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

View File

@@ -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",

View File

@@ -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
}

View 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 JobCardOperation(Document):
pass

View File

@@ -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",

View File

@@ -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",
@@ -178,4 +198,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -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]]
}
};
});
} }
}); });

View File

@@ -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", "doctype": "DocType",
"custom": 0, "document_type": "Setup",
"docstatus": 0, "engine": "InnoDB",
"doctype": "DocType", "field_order": [
"document_type": "Setup", "workstation",
"editable_grid": 0, "data_2",
"engine": "InnoDB", "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, "fieldname": "workstation",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "in_standard_filter": 1,
"fieldname": "workstation", "label": "Default Workstation",
"fieldtype": "Link", "options": "Workstation"
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Default Workstation",
"length": 0,
"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, "fieldname": "section_break_4",
"collapsible": 0, "fieldtype": "Section Break",
"columns": 0, "label": "Operation Description"
"fieldname": "section_break_4", },
"fieldtype": "Section Break",
"hidden": 0,
"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, "fieldname": "description",
"bold": 0, "fieldtype": "Text",
"collapsible": 0, "label": "Description"
"columns": 0, },
"fieldname": "description", {
"fieldtype": "Text", "collapsible": 1,
"hidden": 0, "fieldname": "sub_operations_section",
"ignore_user_permissions": 0, "fieldtype": "Section Break",
"ignore_xss_filter": 0, "label": "Sub Operations"
"in_filter": 0, },
"in_list_view": 0, {
"in_standard_filter": 0, "fieldname": "sub_operations",
"label": "Description", "fieldtype": "Table",
"length": 0, "options": "Sub Operation"
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "description": "Time in mins.",
"print_hide": 0, "fieldname": "total_operation_time",
"print_hide_if_no_value": 0, "fieldtype": "Float",
"read_only": 0, "label": "Total Operation Time",
"remember_last_selected_value": 0, "read_only": 1
"report_hide": 0, },
"reqd": 0, {
"search_index": 0, "fieldname": "data_2",
"set_only_once": 0, "fieldtype": "Column Break"
"unique": 0 },
{
"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, "icon": "fa fa-wrench",
"hide_toolbar": 0, "index_web_pages_for_search": 1,
"icon": "fa fa-wrench", "links": [],
"idx": 0, "modified": "2021-01-12 15:09:23.593338",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Manufacturing",
"name": "Operation",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:28:27.462413",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Operation",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "export": 1,
"create": 1, "import": 1,
"delete": 1, "read": 1,
"email": 0, "role": "Manufacturing User",
"export": 1, "share": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "export": 1,
"create": 1, "import": 1,
"delete": 1, "read": 1,
"email": 0, "report": 1,
"export": 1, "role": "Manufacturing Manager",
"if_owner": 0, "share": 1,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"sort_field": "modified", "track_changes": 1
"sort_order": "DESC",
"track_seen": 0
} }

View File

@@ -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

View File

@@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', {
}, },
download_materials_required: function(frm) { download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; const fields = [{
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); 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';
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) {

View File

@@ -98,7 +98,7 @@ class ProductionPlan(Document):
def get_items(self): def get_items(self):
self.set('po_items', []) self.set('po_items', [])
if self.get_items_from == "Sales Order": if self.get_items_from == "Sales Order":
self.get_so_items() self.get_so_items()
elif self.get_items_from == "Material Request": elif self.get_items_from == "Material Request":
self.get_mr_items() self.get_mr_items()
@@ -170,11 +170,11 @@ class ProductionPlan(Document):
refs = {} refs = {}
for data in items: for data in items:
item_details = get_item_details(data.item_code) item_details = get_item_details(data.item_code)
if self.combine_items: if self.combine_items:
if item_details.bom_no in refs: if item_details.bom_no in refs:
refs[item_details.bom_no]['so_details'].append({ refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent, 'sales_order': data.parent,
'sales_order_item': data.name, 'sales_order_item': data.name,
'qty': data.pending_qty 'qty': data.pending_qty
}) })
refs[item_details.bom_no]['qty'] += data.pending_qty refs[item_details.bom_no]['qty'] += data.pending_qty
@@ -188,10 +188,10 @@ class ProductionPlan(Document):
} }
refs[item_details.bom_no]['so_details'].append({ refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent, 'sales_order': data.parent,
'sales_order_item': data.name, 'sales_order_item': data.name,
'qty': data.pending_qty 'qty': data.pending_qty
}) })
pi = self.append('po_items', { pi = self.append('po_items', {
'include_exploded_items': 1, 'include_exploded_items': 1,
'warehouse': data.warehouse, 'warehouse': data.warehouse,
@@ -209,12 +209,12 @@ class ProductionPlan(Document):
pi.sales_order = data.parent pi.sales_order = data.parent
pi.sales_order_item = data.name pi.sales_order_item = data.name
pi.description = data.description pi.description = data.description
elif self.get_items_from == "Material Request": elif self.get_items_from == "Material Request":
pi.material_request = data.parent pi.material_request = data.parent
pi.material_request_item = data.name pi.material_request_item = data.name
pi.description = data.description pi.description = data.description
if refs: if refs:
for po_item in self.po_items: for po_item in self.po_items:
po_item.planned_qty = refs[po_item.bom_no]['qty'] po_item.planned_qty = refs[po_item.bom_no]['qty']
@@ -477,18 +477,19 @@ class ProductionPlan(Document):
msgprint(_("No material request created")) msgprint(_("No material request created"))
@frappe.whitelist() @frappe.whitelist()
def download_raw_materials(doc): def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
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 +508,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
@@ -677,32 +678,36 @@ 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)
def get_warehouse_list(warehouses, warehouse_list=[]):
if isinstance(warehouses, string_types):
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
@frappe.whitelist() @frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None): def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
warehouse_list = [] warehouse_list = []
if warehouses: if warehouses:
if isinstance(warehouses, string_types): get_warehouse_list(warehouses, warehouse_list)
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
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
@@ -795,7 +800,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)

View File

@@ -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:
@@ -21,4 +31,4 @@ class Routing(Document):
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
.format(row.idx, row.sequence_id, sequence_id)) .format(row.idx, row.sequence_id, sequence_id))
sequence_id = row.sequence_id sequence_id = row.sequence_id

View File

@@ -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)

View File

@@ -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) {
// }
});

View File

@@ -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
}

View 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

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSubOperation(unittest.TestCase):
pass

View File

@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
ste.submit() ste.submit()
stock_entries.append(ste) stock_entries.append(ste)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
self.assertEqual(len(job_cards), len(bom.operations)) self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards): for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card) doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", { doc.time_logs[0].completed_qty = 1
"from_time": add_to_date(None, i),
"hours": 1,
"to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.submit() doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))

View File

@@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
} }
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length && frm.doc.operations && frm.doc.operations.length) {
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
const not_completed = frm.doc.operations.filter(d => { const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') { if(d.status != 'Completed') {
@@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [ fields: [
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'operation', fieldname: 'operation',
label: __('Operation'), label: __('Operation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'workstation', fieldname: 'workstation',
label: __('Workstation'), label: __('Workstation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Data', fieldtype: 'Data',
fieldname:'name', fieldname: 'name',
label: __('Operation Id') label: __('Operation Id')
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'pending_qty', fieldname: 'pending_qty',
label: __('Pending Qty'), label: __('Pending Qty'),
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'qty', fieldname: 'qty',
label: __('Quantity to Manufacture'), label: __('Quantity to Manufacture'),
read_only:0, read_only: 0,
in_list_view:1, in_list_view: 1,
},
{
fieldtype: 'Float',
fieldname: 'batch_size',
label: __('Batch Size'),
read_only: 1
}, },
], ],
data: operations_data, data: operations_data,
@@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) { }, function(data) {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true,
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
operations: data.operations, operations: data.operations,
},
callback: function() {
frm.reload_doc();
} }
}); });
}, __("Job Card"), __("Create")); }, __("Job Card"), __("Create"));
@@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) { if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty); pending_qty = frm.doc.qty - flt(data.completed_qty);
dialog.fields_dict.operations.df.data.push({ if (pending_qty) {
'name': data.name, dialog.fields_dict.operations.df.data.push({
'operation': data.operation, 'name': data.name,
'workstation': data.workstation, 'operation': data.operation,
'qty': pending_qty, 'workstation': data.workstation,
'pending_qty': pending_qty, 'batch_size': data.batch_size,
}); 'qty': pending_qty,
'pending_qty': pending_qty
});
}
} }
}); });
dialog.fields_dict.operations.grid.refresh(); dialog.fields_dict.operations.grid.refresh();
@@ -704,6 +716,8 @@ erpnext.work_order = {
stop_work_order: function(frm, status) { stop_work_order: function(frm, status) {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
freeze: true,
freeze_message: __("Updating Work Order status"),
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
status: status status: status

View File

@@ -21,6 +21,12 @@
"produced_qty", "produced_qty",
"sales_order", "sales_order",
"project", "project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
@@ -52,6 +58,7 @@
"actual_operating_cost", "actual_operating_cost",
"additional_operating_cost", "additional_operating_cost",
"column_break_24", "column_break_24",
"corrective_operation_cost",
"total_operating_cost", "total_operating_cost",
"more_info", "more_info",
"description", "description",
@@ -488,6 +495,57 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Lead Time", "label": "Lead Time",
"read_only": 1 "read_only": 1
},
{
"collapsible": 1,
"depends_on": "eval:!doc.__islocal",
"fieldname": "serial_no_and_batch_for_finished_good_section",
"fieldtype": "Section Break",
"label": "Serial No and Batch for Finished Good"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "production_item.has_serial_no",
"fieldname": "has_serial_no",
"fieldtype": "Check",
"label": "Has Serial No",
"read_only": 1
},
{
"default": "0",
"fetch_from": "production_item.has_batch_no",
"fieldname": "has_batch_no",
"fieldtype": "Check",
"label": "Has Batch No",
"read_only": 1
},
{
"depends_on": "has_serial_no",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial Nos",
"no_copy": 1
},
{
"default": "0",
"depends_on": "has_batch_no",
"fieldname": "batch_size",
"fieldtype": "Float",
"label": "Batch Size"
},
{
"allow_on_submit": 1,
"description": "From Corrective Job Card",
"fieldname": "corrective_operation_cost",
"fieldtype": "Currency",
"label": "Corrective Operation Cost",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
@@ -495,7 +553,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-16 13:27:51.116484", "modified": "2021-06-20 15:19:14.902699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, 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
import frappe import frappe
import json import json
import math import math
@@ -19,18 +18,17 @@ from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
class OverProductionError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass
class SerialNoQtyError(frappe.ValidationError):
pass
from six import string_types
form_grid_templates = {
"operations": "templates/form_grid/work_order_grid.html"
}
class WorkOrder(Document): class WorkOrder(Document):
def onload(self): def onload(self):
@@ -127,7 +125,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_operating_cost \ variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_operating_cost else self.planned_operating_cost
self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
self.total_operating_cost = (flt(self.additional_operating_cost)
+ flt(variable_cost) + flt(self.corrective_operation_cost))
def validate_work_order_against_so(self): def validate_work_order_against_so(self):
# already ordered qty # already ordered qty
@@ -235,12 +235,15 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
def before_submit(self):
self.create_serial_no_batch_no()
def on_submit(self): def on_submit(self):
if not self.wip_warehouse: if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse: if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit")) frappe.throw(_("For Warehouse is required before Submit"))
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
self.update_work_order_qty_in_combined_so() self.update_work_order_qty_in_combined_so()
else: else:
@@ -260,12 +263,76 @@ class WorkOrder(Document):
self.update_work_order_qty_in_combined_so() self.update_work_order_qty_in_combined_so()
else: else:
self.update_work_order_qty_in_so() self.update_work_order_qty_in_so()
self.delete_job_card() self.delete_job_card()
self.update_completed_qty_in_material_request() self.update_completed_qty_in_material_request()
self.update_planned_qty() self.update_planned_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_production() self.update_reserved_qty_for_production()
self.delete_auto_created_batch_and_serial_no()
def create_serial_no_batch_no(self):
if not (self.has_serial_no or self.has_batch_no):
return
if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
return
if self.has_batch_no:
self.create_batch_for_finished_good()
args = {
"item_code": self.production_item,
"work_order": self.name
}
if self.has_serial_no:
self.make_serial_nos(args)
def create_batch_for_finished_good(self):
total_qty = self.qty
if not self.batch_size:
self.batch_size = total_qty
while total_qty > 0:
qty = self.batch_size
if self.batch_size >= total_qty:
qty = total_qty
if total_qty > self.batch_size:
total_qty -= self.batch_size
else:
qty = total_qty
total_qty = 0
make_batch(frappe._dict({
"item": self.production_item,
"qty_to_produce": qty,
"reference_doctype": self.doctype,
"reference_name": self.name
}))
def delete_auto_created_batch_and_serial_no(self):
for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
frappe.delete_doc("Serial No", row.name)
self.db_set("serial_no", "")
for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args):
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
auto_make_serial_nos(args)
serial_nos_length = len(get_serial_nos(self.serial_no))
if serial_nos_length != self.qty:
frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
.format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
def create_job_card(self): def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@@ -273,32 +340,40 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations): for index, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row) qty = self.qty
while qty > 0:
if not row.workstation: qty = split_qty_based_on_batch_size(self, row, qty)
frappe.throw(_("Row {0}: select the workstation against the operation {1}") if row.job_card_qty > 0:
.format(row.idx, row.operation)) self.prepare_data_for_job_card(row, index,
plan_days, enable_capacity_planning)
original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row,
enable_capacity_planning=enable_capacity_planning, auto_create=True)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[-1].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.message_log.pop()
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
planned_end_date = self.operations and self.operations[-1].planned_end_time planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date: if planned_end_date:
self.db_set("planned_end_date", planned_end_date) self.db_set("planned_end_date", planned_end_date)
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row)
if not row.workstation:
frappe.throw(_("Row {0}: select the workstation against the operation {1}")
.format(row.idx, row.operation))
original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row, auto_create=True,
enable_capacity_planning=enable_capacity_planning)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[-1].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.message_log.pop()
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
def set_operation_start_end_time(self, idx, row): def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as """Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation.""" `planned_start_date`, else add time diff to end time of earlier operation."""
@@ -365,7 +440,7 @@ class WorkOrder(Document):
work_order_qty = qty[0][0] if qty and qty[0][0] else 0 work_order_qty = qty[0][0] if qty and qty[0][0] else 0
frappe.db.set_value('Sales Order Item', frappe.db.set_value('Sales Order Item',
self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
def update_work_order_qty_in_combined_so(self): def update_work_order_qty_in_combined_so(self):
total_bundle_qty = 1 total_bundle_qty = 1
if self.product_bundle_item: if self.product_bundle_item:
@@ -378,7 +453,7 @@ class WorkOrder(Document):
prod_plan = frappe.get_doc('Production Plan', self.production_plan) prod_plan = frappe.get_doc('Production Plan', self.production_plan)
item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item')
for plan_reference in prod_plan.prod_plan_references: for plan_reference in prod_plan.prod_plan_references:
work_order_qty = 0.0 work_order_qty = 0.0
if plan_reference.item_reference == item_reference: if plan_reference.item_reference == item_reference:
@@ -386,53 +461,54 @@ class WorkOrder(Document):
work_order_qty = flt(plan_reference.qty) / total_bundle_qty work_order_qty = flt(plan_reference.qty) / total_bundle_qty
frappe.db.set_value('Sales Order Item', frappe.db.set_value('Sales Order Item',
plan_reference.sales_order_item, 'work_order_qty', work_order_qty) plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
def update_completed_qty_in_material_request(self): def update_completed_qty_in_material_request(self):
if self.material_request: if self.material_request:
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
def set_work_order_operations(self): def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'""" """Fetch operations from BOM and set in 'Work Order'"""
self.set('operations', [])
def _get_operations(bom_no, qty=1):
return frappe.db.sql(
f"""select
operation, description, workstation, idx,
base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
"Pending" as status, parent as bom, batch_size, sequence_id
from
`tabBOM Operation`
where
parent = %s order by idx
""", bom_no, as_dict=1)
self.set('operations', [])
if not self.bom_no: if not self.bom_no:
return return
if self.use_multi_level_bom: operations = []
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() if not self.use_multi_level_bom:
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else: else:
bom_list = [self.bom_no] bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
bom_traversal = list(reversed(bom_tree.level_order_traversal()))
bom_traversal.append(bom_tree) # add operation on top level item last
for d in bom_traversal:
if d.is_bom:
operations.extend(_get_operations(d.name, qty=d.exploded_qty))
for correct_index, operation in enumerate(operations, start=1):
operation.idx = correct_index
operations = frappe.db.sql("""
select
operation, description, workstation, idx,
base_hour_rate as hour_rate, time_in_mins,
"Pending" as status, parent as bom, batch_size, sequence_id
from
`tabBOM Operation`
where
parent in (%s) order by idx
""" % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
self.set('operations', operations) self.set('operations', operations)
if self.use_multi_level_bom and self.get('operations') and self.get('items'):
raw_material_operations = [d.operation for d in self.get('items')]
operations = [d.operation for d in self.get('operations')]
for operation in raw_material_operations:
if operation not in operations:
self.append('operations', {
'operation': operation
})
self.calculate_time() self.calculate_time()
def calculate_time(self): def calculate_time(self):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
for d in self.get("operations"): for d in self.get("operations"):
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost() self.calculate_operating_cost()
@@ -669,6 +745,17 @@ class WorkOrder(Document):
bom.set_bom_material_details() bom.set_bom_material_details()
return bom return bom
def update_batch_produced_qty(self, stock_entry_doc):
if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
return
for row in stock_entry_doc.items:
if row.batch_no and (row.is_finished_item or row.is_scrap_item):
qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@@ -746,7 +833,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
return wo_doc return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
if isinstance(variant_items, string_types): if isinstance(variant_items, str):
variant_items = json.loads(variant_items) variant_items = json.loads(variant_items)
for item in variant_items: for item in variant_items:
@@ -826,6 +913,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.set_stock_entry_type() stock_entry.set_stock_entry_type()
stock_entry.get_items() stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict() return stock_entry.as_dict()
@frappe.whitelist() @frappe.whitelist()
@@ -867,13 +955,47 @@ def query_sales_order(production_item):
@frappe.whitelist() @frappe.whitelist()
def make_job_card(work_order, operations): def make_job_card(work_order, operations):
if isinstance(operations, string_types): if isinstance(operations, str):
operations = json.loads(operations) operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order) work_order = frappe.get_doc('Work Order', work_order)
for row in operations: for row in operations:
row = frappe._dict(row)
validate_operation_data(row) validate_operation_data(row)
create_job_card(work_order, row, row.get("qty"), auto_create=True) qty = row.get("qty")
while qty > 0:
qty = split_qty_based_on_batch_size(work_order, row, qty)
if row.job_card_qty > 0:
create_job_card(work_order, row, auto_create=True)
def split_qty_based_on_batch_size(wo_doc, row, qty):
if not cint(frappe.db.get_value("Operation",
row.operation, "create_job_card_based_on_batch_size")):
row.batch_size = row.get("qty") or wo_doc.qty
row.job_card_qty = row.batch_size
if row.batch_size and qty >= row.batch_size:
qty -= row.batch_size
elif qty > 0:
row.job_card_qty = qty
qty = 0
get_serial_nos_for_job_card(row, wo_doc)
return qty
def get_serial_nos_for_job_card(row, wo_doc):
if not wo_doc.serial_no:
return
serial_nos = get_serial_nos(wo_doc.serial_no)
used_serial_nos = []
for d in frappe.get_all('Job Card', fields=['serial_no'],
filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
used_serial_nos.extend(get_serial_nos(d.serial_no))
serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
def validate_operation_data(row): def validate_operation_data(row):
if row.get("qty") <= 0: if row.get("qty") <= 0:
@@ -892,20 +1014,22 @@ def validate_operation_data(row):
) )
) )
def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card") doc = frappe.new_doc("Job Card")
doc.update({ doc.update({
'work_order': work_order.name, 'work_order': work_order.name,
'operation': row.get("operation"), 'operation': row.get("operation"),
'workstation': row.get("workstation"), 'workstation': row.get("workstation"),
'posting_date': nowdate(), 'posting_date': nowdate(),
'for_quantity': qty or work_order.get('qty', 0), 'for_quantity': row.job_card_qty or work_order.get('qty', 0),
'operation_id': row.get("name"), 'operation_id': row.get("name"),
'bom_no': work_order.bom_no, 'bom_no': work_order.bom_no,
'project': work_order.project, 'project': work_order.project,
'company': work_order.company, 'company': work_order.company,
'sequence_id': row.get("sequence_id"), 'sequence_id': row.get("sequence_id"),
'wip_warehouse': work_order.wip_warehouse 'wip_warehouse': work_order.wip_warehouse,
'hour_rate': row.get("hour_rate"),
'serial_no': row.get("serial_no")
}) })
if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:

View File

@@ -4,10 +4,17 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'work_order', 'fieldname': 'work_order',
'non_standard_fieldnames': {
'Batch': 'reference_name'
},
'transactions': [ 'transactions': [
{ {
'label': _('Transactions'), 'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List'] 'items': ['Stock Entry', 'Job Card', 'Pick List']
},
{
'label': _('Reference'),
'items': ['Serial No', 'Batch']
} }
] ]
} }

View File

@@ -2,14 +2,14 @@
"actions": [], "actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"details", "details",
"operation", "operation",
"bom", "bom",
"sequence_id", "column_break_4",
"description", "description",
"sequence_id",
"col_break1", "col_break1",
"completed_qty", "completed_qty",
"status", "status",
@@ -48,6 +48,7 @@
{ {
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "BOM", "label": "BOM",
"no_copy": 1, "no_copy": 1,
"options": "BOM", "options": "BOM",
@@ -67,6 +68,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "Operation completed for how many finished goods?", "description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
@@ -76,6 +78,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 1,
"default": "Pending", "default": "Pending",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
@@ -118,6 +121,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "in Minutes", "description": "in Minutes",
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
@@ -195,12 +199,16 @@
"label": "Sequence ID", "label": "Sequence ID",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-14 12:58:49.241252", "modified": "2021-06-24 14:36:12.835543",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
@@ -209,4 +217,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
import frappe import frappe
import unittest import unittest
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError
from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing
from frappe.test_runner import make_test_records
test_dependencies = ["Warehouse"] test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation') test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
class TestWorkstation(unittest.TestCase): class TestWorkstation(unittest.TestCase):
def test_validate_timings(self): def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
@@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase):
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
def test_update_bom_operation_rate(self):
operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"time_in_mins": 60
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation B",
"hour_rate_rent": 1000,
"time_in_mins": 60
}
]
for row in operations:
make_workstation(row)
make_operation(row)
test_routing_operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"time_in_mins": 60
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation A",
"time_in_mins": 60
}
]
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")
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
#resets values
w1.hour_rate_rent = 300
w1.hour_rate_labour = 0
w1.save()
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 300)
self.assertEqual(bom_doc.operations[0].hour_rate, 300)
w1.hour_rate_rent = 250
w1.save()
#updating after setting new rates in workstations
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 250)
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
def make_workstation(*args, **kwargs): def make_workstation(*args, **kwargs):
args = args if args else kwargs args = args if args else kwargs
if isinstance(args, tuple): if isinstance(args, tuple):
@@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
"doctype": "Workstation", "doctype": "Workstation",
"workstation_name": workstation_name "workstation_name": workstation_name
}) })
doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert() doc.insert()
return doc return doc
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
return frappe.get_doc("Workstation", workstation_name) return frappe.get_doc("Workstation", workstation_name)

View File

@@ -39,7 +39,8 @@ class Workstation(Document):
def update_bom_operation(self): def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
where workstation = %s""", self.name) where workstation = %s and parenttype = 'routing' """, self.name)
for bom_no in bom_list: for bom_no in bom_list:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
where parent = %s and workstation = %s""", where parent = %s and workstation = %s""",
@@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
operation_length = time_diff_in_seconds(to_datetime, from_datetime) operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation) workstation = frappe.get_doc("Workstation", workstation)
if not workstation.working_hours: if not workstation.working_hours:
return return

View File

@@ -0,0 +1,105 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Cost of Poor Quality Report"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Datetime",
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Datetime",
default: frappe.datetime.now_datetime(),
reqd: 1,
},
{
label: __("Job Card"),
fieldname: "name",
fieldtype: "Link",
options: "Job Card",
get_query: function() {
return {
filters: {
is_corrective_job_card: 1,
docstatus: 1
}
}
}
},
{
label: __("Work Order"),
fieldname: "work_order",
fieldtype: "Link",
options: "Work Order"
},
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
get_query: function() {
return {
filters: {
is_corrective_operation: 1
}
}
}
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation"
},
{
label: __("Item"),
fieldname: "production_item",
fieldtype: "Link",
options: "Item"
},
{
label: __("Serial No"),
fieldname: "serial_no",
fieldtype: "Link",
options: "Serial No",
depends_on: "eval: doc.production_item",
get_query: function() {
var item_code = frappe.query_report.get_filter_value('production_item');
return {
filters: {
item_code: item_code
}
}
}
},
{
label: __("Batch No"),
fieldname: "batch_no",
fieldtype: "Link",
options: "Batch No",
depends_on: "eval: doc.production_item",
get_query: function() {
var item_code = frappe.query_report.get_filter_value('production_item');
return {
filters: {
item: item_code
}
}
}
},
]
};

View File

@@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-01-11 11:10:58.292896",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2021-01-11 11:11:03.594242",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Cost of Poor Quality Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Job Card",
"report_name": "Cost of Poor Quality Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
]
}

View File

@@ -0,0 +1,127 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
columns, data = [], []
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
if operations:
operations = [d.name for d in operations]
fields = ["production_item as item_code", "item_name", "work_order", "operation",
"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
filters = get_filters(report_filters, operations)
job_cards = frappe.get_all("Job Card", fields = fields,
filters = filters)
for row in job_cards:
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
update_raw_material_cost(row, report_filters)
data.append(row)
return data
def get_filters(report_filters, operations):
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
if report_filters.get(field):
if field != 'serial_no':
filters[field] = report_filters.get(field)
else:
filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
return filters
def update_raw_material_cost(row, filters):
row.rm_cost = 0.0
for data in frappe.get_all("Job Card Item", fields = ["amount"],
filters={"parent": row.name, "docstatus": 1}):
row.rm_cost += data.amount
def get_columns(filters):
return [
{
"label": _("Job Card"),
"fieldtype": "Link",
"fieldname": "name",
"options": "Job Card",
"width": "100"
},
{
"label": _("Work Order"),
"fieldtype": "Link",
"fieldname": "work_order",
"options": "Work Order",
"width": "100"
},
{
"label": _("Item Code"),
"fieldtype": "Link",
"fieldname": "item_code",
"options": "Item",
"width": "100"
},
{
"label": _("Item Name"),
"fieldtype": "Data",
"fieldname": "item_name",
"width": "100"
},
{
"label": _("Operation"),
"fieldtype": "Link",
"fieldname": "operation",
"options": "Operation",
"width": "100"
},
{
"label": _("Serial No"),
"fieldtype": "Data",
"fieldname": "serial_no",
"width": "100"
},
{
"label": _("Batch No"),
"fieldtype": "Data",
"fieldname": "batch_no",
"width": "100"
},
{
"label": _("Workstation"),
"fieldtype": "Link",
"fieldname": "workstation",
"options": "Workstation",
"width": "100"
},
{
"label": _("Operating Cost"),
"fieldtype": "Currency",
"fieldname": "operating_cost",
"width": "100"
},
{
"label": _("Raw Material Cost"),
"fieldtype": "Currency",
"fieldname": "rm_cost",
"width": "100"
},
{
"label": _("Total Time (in Mins)"),
"fieldtype": "Float",
"fieldname": "total_time_in_mins",
"width": "100"
}
]

View File

@@ -288,4 +288,7 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details

View File

@@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Buying Settings")
buying_settings = frappe.get_single("Buying Settings")
buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
buying_settings.save()

View File

@@ -0,0 +1,16 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "job_card")
frappe.reload_doc("manufacturing", "doctype", "job_card_item")
frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
SET jc.hour_rate = wo.hour_rate
WHERE
jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
""")

View File

@@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', {
} }
}; };
}); });
},
frm.trigger('set_earning_component'); onload: function(frm) {
if (frm.doc.type) {
frm.trigger('set_component_query');
}
}, },
employee: function(frm) { employee: function(frm) {
@@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
}, },
company: function(frm) { company: function(frm) {
frm.trigger('set_earning_component'); frm.set_value("type", "");
frm.trigger('set_component_query');
}, },
set_earning_component: function(frm) { set_component_query: function(frm) {
if (!frm.doc.company) return; if (!frm.doc.company) return;
let filters = {company: frm.doc.company};
if (frm.doc.type) {
filters.type = frm.doc.type;
}
frm.set_query("salary_component", function() { frm.set_query("salary_component", function() {
return { return {
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} filters: filters
}; };
}); });
}, },

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