Merge branch 'develop' into patch-4

This commit is contained in:
Deepesh Garg 2021-07-01 18:08:22 +05:30 committed by GitHub
commit 8a5c7ea9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
165 changed files with 4516 additions and 1108 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.5.1'
__version__ = '13.6.0'
def get_default_company(user=None):
'''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',
{'parent': address, 'link_name': company}):
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 {}

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,
total_days, total_booking_days, account_currency)
if not amount:
return
if via_journal_entry:
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)

View File

@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
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)
frappe.throw(msg)

View File

@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
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:
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
if rate_of_interest:
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)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {

View File

@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.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,
btn: $(btn_primary),
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
def publish(index, total, doctype):
if total < 5: return
if total < 50:
return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
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', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) {
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);

View File

@ -690,7 +690,7 @@
"options": "Account"
},
{
"depends_on": "eval:doc.received_amount",
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
"label": "Received Amount After Tax",
@ -707,7 +707,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-09 11:55:04.215050",
"modified": "2021-06-22 20:37:06.154206",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -706,7 +706,7 @@ class PaymentEntry(AccountsController):
if account_currency != 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"
elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
@ -761,7 +761,7 @@ class PaymentEntry(AccountsController):
return self.advance_tax_account
elif self.payment_type == 'Receive':
return self.paid_from
elif self.payment_type == 'Pay':
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_to
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() {
super.onload();
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm,
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')
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"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@ -634,6 +636,34 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project
}, 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 self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@ -1141,6 +1171,36 @@ class PurchaseInvoice(BuyingController):
if update:
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):
from erpnext.controllers.website_list_for_contact import get_list_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][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):
pi = frappe.copy_doc(test_records[1])
pi.insert()
@ -966,7 +987,7 @@ class TestPurchaseInvoice(unittest.TestCase):
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# 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.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
@ -1002,6 +1023,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name)
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.save()
purchase_invoice.submit()

View File

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

View File

@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si)
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():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
@ -1985,33 +2012,6 @@ def get_sales_invoice_for_e_invoice():
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():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
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):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project']
'cost_center', 'project', 'voucher_detail_no']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions

View File

@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"account",
"label": __("Account"),
"fieldtype": "Link",
"fieldtype": "MultiSelectList",
"options": "Account",
"get_query": function() {
var company = frappe.query_report.get_filter_value('company');
return {
"doctype": "Account",
"filters": {
"company": company,
}
}
get_data: function(txt) {
return frappe.db.get_link_options('Account', txt, {
company: frappe.query_report.get_filter_value("company")
});
}
},
{
@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"),
"fieldtype": "MultiSelectList",
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"),
"fieldtype": "MultiSelectList",
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"):
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):
frappe.throw(_("Account {0} does not exists").format(filters.account))
for account in 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')
and account_details[filters.account].is_group == 0):
@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None
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"):
gle_currency = frappe.db.get_value(
"GL Entry", {
@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
conditions.append("account in %(account)s")
if filters.get("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 ""
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):
data = []

View File

@ -168,21 +168,24 @@ def get_columns(filters):
"label": _("Income"),
"fieldtype": "Currency",
"options": "currency",
"width": 120
"width": 305
},
{
"fieldname": "expense",
"label": _("Expense"),
"fieldtype": "Currency",
"options": "currency",
"width": 120
"width": 305
},
{
"fieldname": "gross_profit_loss",
"label": _("Gross Profit / Loss"),
"fieldtype": "Currency",
"options": "currency",
"width": 120
"width": 307
}
]

View File

@ -9,13 +9,14 @@
"supp_master_name",
"supplier_group",
"buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_3",
"po_required",
"pr_required",
"maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@ -108,6 +109,13 @@
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"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",
@ -115,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-04 20:01:44.087066",
"modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator",
"module": "Buying",
"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')
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")
.format(item.item_code, item.idx, max_allowed_amt))
if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
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"])
return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active'
where status in ('Active', 'Suspended')
and docstatus < 2
and ({key} 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`
where
`tabProject`.status not in ("Completed", "Cancelled")
and {cond} {match_cond} {scond}
and {cond} {scond} {match_cond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
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}")
.format(d.idx, s, doc.doctype, doc.return_against))
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \
and not d.get("warehouse"):
frappe.throw(_("Warehouse is mandatory"))
if (warehouse_mandatory and not d.get("warehouse") and
frappe.db.get_value("Item", d.item_code, "is_stock_item")
):
frappe.throw(_("Warehouse is mandatory"))
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):
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
if self.is_internal_transfer():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
if d.doctype == "Packed Item":
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
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
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.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
@ -497,9 +497,6 @@ class StockController(AccountsController):
})
if future_sle_exists(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()
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) {
return {
data: {
labels: result.map(d => d[0]),
labels: result.map(d => d.creation_date),
datasets: [{
name: "First Response Time",
values: result.map(d => d[1])
values: result.map(d => d.first_response_time)
}]
},
type: "line",
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
hide_days: 0,
hide_seconds: 0
};
value = frappe.utils.get_formatted_duration(d, duration_options);
return value;
return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import unittest
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
class TestPatientHistorySettings(unittest.TestCase):
@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase):
self.assertTrue(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()))
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.communication_date, getdate())
@ -101,4 +101,4 @@ def create_doc(patient):
}).insert()
doc.submit()
return doc
return doc

View File

@ -158,6 +158,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
{"from_route": "/project", "to_route": "Project"}
]
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) {
return{
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"])
self.validate_attendance_date()
self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record()
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.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):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date

View File

@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);

View File

@ -207,7 +207,7 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Active\nInactive\nLeft",
"options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1,
"search_index": 1
},
@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2021-06-12 11:31:37.730760",
"modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
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 import throw, _, scrub
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 erpnext.utilities.transaction_base import delete_events
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 EmployeeLeftValidationError(frappe.ValidationError): pass
@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self):
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.set_employee_name()

View File

@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee',
'non_standard_fieldnames': {
'Bank Account': 'party'
'Bank Account': 'party',
'Employee Grievance': 'raised_by'
},
'transactions': [
{
@ -20,7 +21,7 @@ def get_data():
},
{
'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
},
{
'label': _('Shift'),

View File

@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]],
get_indicator: function(doc) {
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;
}
};

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
frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"],
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status];

View File

@ -110,6 +110,7 @@
"label": "Allocation"
},
{
"allow_on_submit": 1,
"bold": 1,
"fieldname": "new_leaves_allocated",
"fieldtype": "Float",
@ -235,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-14 15:28:26.335104",
"modified": "2021-06-03 15:28:26.335104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@ -277,4 +278,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "employee"
}
}

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
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_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward:
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):
allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1,
@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type):
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
import frappe
import erpnext
import unittest
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_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase):
@classmethod
def setUpClass(cls):
@ -164,6 +164,51 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel()
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):
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);
}
}
};

View File

@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 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)
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 \
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} \
for {2} as per staffing plan {3} for parent company {4}."
.format(cint(parent_plan_details[0].vacancies),
for {2} as per staffing plan {3} for parent company {4}.").format(
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name,
parent_company)), ParentCompanyError)
parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company
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(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}. \
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
.format(cint(all_sibling_details.vacancies),
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_company,
cint(parent_plan_details[0].vacancies),
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):
#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 \
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}. \
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
.format(self.company,
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
self.company,
cint(children_details.vacancies),
children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist()
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)
# 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",
"idx": 0,
"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>",
"modified": "2021-05-24 16:29:13.165930",
"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-06-16 14:08:12.933367",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Scheduled",

View File

@ -24,19 +24,19 @@
{% set start = frappe.utils.get_datetime(doc.start_time) %}
{% set end = frappe.utils.get_datetime(doc.end_time) %}
{% if start.date() == end.date() %}
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li>
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li>
{% else %}
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
</li>
<li>{{_("End Time")}}: <b>{{ end.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>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% 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 %}
<li>Note: This Training Event is mandatory</li>
<li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %}
</ul>
</div>
@ -44,4 +44,4 @@
<td width="15"></td>
</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
FROM `tabLeave Ledger Entry`
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
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -153,6 +153,24 @@
"onboard": 0,
"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",
"hidden": 0,
@ -823,7 +841,7 @@
"type": "Link"
}
],
"modified": "2021-04-26 13:36:15.413819",
"modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

View File

@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
refresh: function(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
toggle_operations(frm);
frm.set_indicator_formatter('item_code',
function(doc) {
@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true,
args: {
update_parent: true,
from_child_bom:false,
save: frm.doc.docstatus === 1 ? true : false
from_child_bom:false
},
callback: function(r) {
refresh_field("items");
@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
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) {
if(!cint(frm.doc.with_operations)) {
frm.set_value("operations", []);
}
toggle_operations(frm);
});

View File

@ -193,6 +193,7 @@
},
{
"default": "Work Order",
"depends_on": "with_operations",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
@ -235,6 +236,7 @@
{
"fieldname": "operations_section",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break"
},
{
@ -245,6 +247,7 @@
"options": "Routing"
},
{
"depends_on": "with_operations",
"fieldname": "operations",
"fieldtype": "Table",
"label": "Operations",
@ -517,7 +520,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2020-05-21 12:29:32.634952",
"modified": "2021-03-16 12:25:09.081968",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@ -1,7 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# 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
from frappe.utils import cint, cstr, flt, today
from frappe import _
@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
import functools
from six import string_types
from operator import itemgetter
form_grid_templates = {
"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):
website = frappe._dict(
# page_title_field = "item_name",
@ -81,7 +153,7 @@ class BOM(WebsiteGenerator):
self.validate_operations()
self.calculate_cost()
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):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@ -152,7 +224,7 @@ class BOM(WebsiteGenerator):
if not args:
args = frappe.form_dict.get('args')
if isinstance(args, string_types):
if isinstance(args, str):
import json
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)
@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:
return
@ -242,7 +314,7 @@ class BOM(WebsiteGenerator):
if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True
self.calculate_cost()
self.calculate_cost(update_hour_rate)
if save:
self.db_update()
@ -403,32 +475,47 @@ class BOM(WebsiteGenerator):
bom_list.reverse()
return bom_list
def calculate_cost(self):
def calculate_cost(self, update_hour_rate = False):
"""Calculate bom totals"""
self.calculate_op_cost()
self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost()
self.calculate_sm_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
def calculate_op_cost(self):
def calculate_op_cost(self, update_hour_rate = False):
"""Update workstation rate and calculates totals"""
self.operating_cost = 0
self.base_operating_cost = 0
for d in self.get('operations'):
if d.workstation:
if not d.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.update_rate_and_time(d, update_hour_rate)
self.operating_cost += flt(d.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):
"""Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0
@ -575,7 +662,7 @@ class BOM(WebsiteGenerator):
self.get_routing()
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"))
if self.with_operations:
@ -585,6 +672,11 @@ class BOM(WebsiteGenerator):
if not d.batch_size or d.batch_size <= 0:
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):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
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"):
query_filters["is_stock_item"] = 1
return frappe.get_all("Item",
fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by,

View File

@ -2,14 +2,13 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
from collections import deque
import unittest
import frappe
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
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 six import string_types
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.tests.test_subcontracting import set_backflush_based_on
@ -123,7 +122,7 @@ class TestBOM(unittest.TestCase):
bom.items[0].conversion_factor = 5
bom.insert()
bom.update_cost()
bom.update_cost(update_hour_rate = False)
# test amounts in selected currency
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])
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"):
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):
if warehouse_list and isinstance(warehouse_list, string_types):
if warehouse_list and isinstance(warehouse_list, str):
warehouse_list = [warehouse_list]
if not warehouse_list:

View File

@ -13,10 +13,10 @@
"col_break1",
"hour_rate",
"time_in_mins",
"batch_size",
"operating_cost",
"base_hour_rate",
"base_operating_cost",
"batch_size",
"image"
],
"fields": [
@ -61,6 +61,8 @@
},
{
"description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@ -104,7 +106,8 @@
"label": "Image"
},
{
"default": "1",
"fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
@ -120,7 +123,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-13 18:14:10.018774",
"modified": "2021-01-12 14:48:09.596843",
"modified_by": "Administrator",
"module": "Manufacturing",
"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) {
@ -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() {
return {
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");
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.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) {
frm.trigger("toggle_operation_number");
@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
frm.add_custom_button(__("Start"), () => {
if (!frm.doc.employee) {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
fieldname: 'employee'}, d => {
if (d.employee) {
frm.set_value("employee", d.employee);
} else {
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
if (!frm.doc.started_time && !frm.doc.current_time) {
frm.add_custom_button(__("Start Job"), () => {
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
options: "Job Card Time Log", fieldname: 'employees'}, d => {
frm.events.start_job(frm, "Work In Progress", d.employees);
}, __("Assign Job to Employee"));
} else {
frm.events.start_job(frm);
frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
}
}).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") {
frm.add_custom_button(__("Resume"), () => {
frappe.flags.resume_job = 1;
frm.events.start_job(frm);
frm.add_custom_button(__("Resume Job"), () => {
frm.events.start_job(frm, "Resume Job", frm.doc.employee);
}).addClass("btn-primary");
} else {
frm.add_custom_button(__("Pause"), () => {
frappe.flags.pause_job = 1;
frm.set_value("status", "On Hold");
frm.events.complete_job(frm);
frm.add_custom_button(__("Pause Job"), () => {
frm.events.complete_job(frm, "On Hold");
});
frm.add_custom_button(__("Complete"), () => {
let completed_time = frappe.datetime.now_datetime();
frm.trigger("hide_timer");
frm.add_custom_button(__("Complete Job"), () => {
var sub_operations = frm.doc.sub_operations;
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'),
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, completed_time, data.qty);
}, __("Enter Value"), __("Complete"));
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value"));
} else {
frm.events.complete_job(frm, completed_time, 0);
frm.events.complete_job(frm, "Complete", 0.0);
}
}).addClass("btn-primary");
}
},
start_job: function(frm) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
row.from_time = frappe.datetime.now_datetime();
frm.set_value('job_started', 1);
frm.set_value('started_time' , row.from_time);
frm.set_value("status", "Work In Progress");
if (!frappe.flags.resume_job) {
frm.set_value('current_time' , 0);
}
frm.save();
start_job: function(frm, status, employee) {
const args = {
job_card_id: frm.doc.name,
start_time: frappe.datetime.now_datetime(),
employees: employee,
status: status
};
frm.events.make_time_log(frm, args);
},
complete_job: function(frm, completed_time, completed_qty) {
frm.doc.time_logs.forEach(d => {
if (d.from_time && !d.to_time) {
d.to_time = completed_time || frappe.datetime.now_datetime();
d.completed_qty = completed_qty || 0;
complete_job: function(frm, status, completed_qty) {
const args = {
job_card_id: frm.doc.name,
complete_time: frappe.datetime.now_datetime(),
status: status,
completed_qty: completed_qty
};
frm.events.make_time_log(frm, args);
},
if(frappe.flags.pause_job) {
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
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);
}
make_time_log: function(frm, args) {
frm.events.update_sub_operation(frm, args);
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) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
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) {
frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
},
make_dashboard: function(frm) {
@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
},
to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', '');
}
})

View File

@ -9,38 +9,49 @@
"naming_series",
"work_order",
"bom_no",
"workstation",
"operation",
"operation_row_number",
"column_break_4",
"posting_date",
"company",
"remarks",
"production_section",
"production_item",
"item_name",
"for_quantity",
"quality_inspection",
"wip_warehouse",
"serial_no",
"column_break_12",
"employee",
"employee_name",
"status",
"wip_warehouse",
"quality_inspection",
"project",
"batch_no",
"operation_section_section",
"operation",
"operation_row_number",
"column_break_18",
"workstation",
"employee",
"section_break_21",
"sub_operations",
"timing_detail",
"time_logs",
"section_break_13",
"total_completed_qty",
"total_time_in_mins",
"column_break_15",
"total_time_in_mins",
"section_break_8",
"items",
"corrective_operation_section",
"for_job_card",
"is_corrective_job_card",
"column_break_33",
"hour_rate",
"for_operation",
"more_information",
"operation_id",
"sequence_id",
"transferred_qty",
"requested_qty",
"status",
"column_break_20",
"remarks",
"barcode",
"job_started",
"started_time",
@ -117,13 +128,6 @@
"fieldtype": "Section Break",
"label": "Timing Detail"
},
{
"fieldname": "employee",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
},
{
"allow_bulk_edit": 1,
"fieldname": "time_logs",
@ -133,9 +137,11 @@
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"default": "0",
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"label": "Total Completed Qty",
@ -160,8 +166,7 @@
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Job Card Item",
"read_only": 1
"options": "Job Card Item"
},
{
"collapsible": 1,
@ -251,12 +256,7 @@
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Read Only",
"label": "Employee Name"
},
{
"collapsible": 1,
"fieldname": "production_section",
"fieldtype": "Section Break",
"label": "Production"
@ -314,11 +314,89 @@
"label": "Quality Inspection",
"no_copy": 1,
"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,
"links": [],
"modified": "2020-11-19 18:26:50.531664",
"modified": "2021-03-16 15:59:32.766484",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@ -5,11 +5,12 @@
from __future__ import unicode_literals
import frappe
import datetime
import json
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
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
@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status()
self.validate_operation_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):
self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
if 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
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 = 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):
production_capacity = 1
@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee:
if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": self.employee,
"employee": args.get("employee"),
"workstation": self.workstation
}, as_dict=True)
@ -158,6 +173,100 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date,
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):
self.append("time_logs", {
"from_time": row.planned_start_time,
@ -182,15 +291,18 @@ class JobCard(Document):
if self.get('operation') == d.operation:
self.append('items', {
'item_code': d.item_code,
'source_warehouse': d.source_warehouse,
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
'item_name': d.item_name,
'description': d.description,
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
"item_code": d.item_code,
"source_warehouse": d.source_warehouse,
"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
"item_name": d.item_name,
"description": d.description,
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount
})
def on_submit(self):
self.validate_transfer_qty()
self.validate_job_card()
self.update_work_order()
self.set_transferred_qty()
@ -199,7 +311,16 @@ class JobCard(Document):
self.update_work_order()
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):
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:
frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@ -215,6 +336,10 @@ class JobCard(Document):
if not self.work_order:
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
from_time_list, to_time_list = [], []
@ -225,10 +350,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins)
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.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):
if self.docstatus < 2: return
@ -248,8 +387,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s
and jc.operation_id = %s and jc.docstatus = 1
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations:
@ -271,7 +410,8 @@ class JobCard(Document):
def get_current_operation_data(self):
return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
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):
for row in ste_doc.items:
@ -354,7 +494,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError)
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
data = self.get_current_operation_data()
@ -376,6 +520,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.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()
def get_operation_details(work_order, operation):
if work_order and operation:
@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data)
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",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1
"options": "Item"
},
{
"fieldname": "source_warehouse",
@ -67,8 +66,7 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"read_only": 1
"label": "Required Qty"
},
{
"fieldname": "column_break_9",
@ -107,7 +105,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-11 13:50:13.804108",
"modified": "2021-04-22 18:50:00.003444",
"modified_by": "Administrator",
"module": "Manufacturing",
"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",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"from_time",
"to_time",
"column_break_2",
"time_in_mins",
"completed_qty"
"completed_qty",
"operation"
],
"fields": [
{
@ -41,10 +44,27 @@
"in_list_view": 1,
"label": "Completed Qty",
"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,
"modified": "2019-12-03 12:56:02.285448",
"links": [],
"modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",

View File

@ -26,7 +26,10 @@
"column_break_16",
"overproduction_percentage_for_work_order",
"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": [
{
@ -155,13 +158,30 @@
{
"fieldname": "column_break_5",
"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",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 10:55:43.996581",
"modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@ -178,4 +198,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@ -2,7 +2,13 @@
// For license information, please see license.txt
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,
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2014-11-07 16:20:30.683186",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2014-11-07 16:20:30.683186",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"workstation",
"data_2",
"is_corrective_operation",
"job_card_section",
"create_job_card_based_on_batch_size",
"column_break_6",
"batch_size",
"sub_operations_section",
"sub_operations",
"total_operation_time",
"section_break_4",
"description"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "workstation",
"fieldtype": "Link",
"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
},
"fieldname": "workstation",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Default Workstation",
"options": "Workstation"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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
},
"collapsible": 1,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Operation Description"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
},
{
"collapsible": 1,
"fieldname": "sub_operations_section",
"fieldtype": "Section Break",
"label": "Sub Operations"
},
{
"fieldname": "sub_operations",
"fieldtype": "Table",
"options": "Sub Operation"
},
{
"description": "Time in mins.",
"fieldname": "total_operation_time",
"fieldtype": "Float",
"label": "Total Operation Time",
"read_only": 1
},
{
"fieldname": "data_2",
"fieldtype": "Column Break"
},
{
"default": "1",
"depends_on": "create_job_card_based_on_batch_size",
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size",
"mandatory_depends_on": "create_job_card_based_on_batch_size"
},
{
"default": "0",
"fieldname": "create_job_card_based_on_batch_size",
"fieldtype": "Check",
"label": "Create Job Card based on Batch Size"
},
{
"collapsible": 1,
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_corrective_operation",
"fieldtype": "Check",
"label": "Is Corrective Operation"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-wrench",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"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",
],
"icon": "fa fa-wrench",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-12 15:09:23.593338",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Operation",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 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,
"create": 1,
"delete": 1,
"export": 1,
"import": 1,
"read": 1,
"role": "Manufacturing User",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"export": 1,
"import": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -2,9 +2,34 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class Operation(Document):
def validate(self):
if not self.description:
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) {
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 });
const fields = [{
fieldname: 'warehouses',
fieldtype: 'Table MultiSelect',
label: __('Warehouses'),
default: frm.doc.from_warehouse,
options: "Production Plan Material Request Warehouse",
get_query: function () {
return {
filters: {
company: frm.doc.company
}
};
},
}];
frappe.prompt(fields, (row) => {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
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) {

View File

@ -98,7 +98,7 @@ class ProductionPlan(Document):
def get_items(self):
self.set('po_items', [])
if self.get_items_from == "Sales Order":
self.get_so_items()
self.get_so_items()
elif self.get_items_from == "Material Request":
self.get_mr_items()
@ -170,11 +170,11 @@ class ProductionPlan(Document):
refs = {}
for data in items:
item_details = get_item_details(data.item_code)
if self.combine_items:
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]['so_details'].append({
'sales_order': data.parent,
'sales_order_item': data.name,
'sales_order_item': data.name,
'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({
'sales_order': data.parent,
'sales_order_item': data.name,
'sales_order_item': data.name,
'qty': data.pending_qty
})
pi = self.append('po_items', {
'include_exploded_items': 1,
'warehouse': data.warehouse,
@ -209,12 +209,12 @@ class ProductionPlan(Document):
pi.sales_order = data.parent
pi.sales_order_item = data.name
pi.description = data.description
elif self.get_items_from == "Material Request":
pi.material_request = data.parent
pi.material_request_item = data.name
pi.description = data.description
if refs:
for po_item in self.po_items:
po_item.planned_qty = refs[po_item.bom_no]['qty']
@ -477,18 +477,19 @@ class ProductionPlan(Document):
msgprint(_("No material request created"))
@frappe.whitelist()
def download_raw_materials(doc):
def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
'Safety Stock', 'Required Qty']]
'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned 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'),
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'):
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,
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.purchase_uom, item_uom.conversion_factor
item.purchase_uom, item_uom.conversion_factor, item.safety_stock
from
`tabBOM Explosion Item` bei
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,
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`
where item_code = %(item_code)s {conditions}
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
ifnull(sum(planned_qty),0) as planned_qty
from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse
""".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()
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):
doc = frappe._dict(json.loads(doc))
warehouse_list = []
if warehouses:
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"))
get_warehouse_list(warehouses, warehouse_list)
if 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"))
warehouse_list = None
@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None):
if 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 = []
for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company)

View File

@ -4,14 +4,24 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe.utils import cint, flt
from frappe import _
from frappe.model.document import Document
class Routing(Document):
def validate(self):
self.calculate_operating_cost()
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):
sequence_id = 0
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}")
.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
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase):
@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
wo_doc.cancel()
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):
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
for row in rows:
make_workstation(row)
make_operation(row)
@ -61,12 +105,14 @@ def create_routing(**args):
if not args.do_not_save:
try:
for operation in args.operations:
doc.append("operations", operation)
doc.insert()
except frappe.DuplicateEntryError:
doc = frappe.get_doc("Routing", args.routing_name)
doc.delete_key('operations')
for operation in args.operations:
doc.append("operations", operation)
doc.save()
return doc
@ -91,7 +137,7 @@ def setup_bom(**args):
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
if not name:
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
routing = args.routing, with_operations=1)
routing = args.routing, with_operations=1, currency = args.currency)
else:
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()
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))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {
"from_time": add_to_date(None, i),
"hours": 1,
"to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.time_logs[0].completed_qty = 1
doc.submit()
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
&& frm.doc.operations && frm.doc.operations.length
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
&& frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') {
@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [
{
fieldtype:'Link',
fieldname:'operation',
fieldtype: 'Link',
fieldname: 'operation',
label: __('Operation'),
read_only:1,
in_list_view:1
read_only: 1,
in_list_view: 1
},
{
fieldtype:'Link',
fieldname:'workstation',
fieldtype: 'Link',
fieldname: 'workstation',
label: __('Workstation'),
read_only:1,
in_list_view:1
read_only: 1,
in_list_view: 1
},
{
fieldtype:'Data',
fieldname:'name',
fieldtype: 'Data',
fieldname: 'name',
label: __('Operation Id')
},
{
fieldtype:'Float',
fieldname:'pending_qty',
fieldtype: 'Float',
fieldname: 'pending_qty',
label: __('Pending Qty'),
},
{
fieldtype:'Float',
fieldname:'qty',
fieldtype: 'Float',
fieldname: 'qty',
label: __('Quantity to Manufacture'),
read_only:0,
in_list_view:1,
read_only: 0,
in_list_view: 1,
},
{
fieldtype: 'Float',
fieldname: 'batch_size',
label: __('Batch Size'),
read_only: 1
},
],
data: operations_data,
@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true,
args: {
work_order: frm.doc.name,
operations: data.operations,
},
callback: function() {
frm.reload_doc();
}
});
}, __("Job Card"), __("Create"));
@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty);
dialog.fields_dict.operations.df.data.push({
'name': data.name,
'operation': data.operation,
'workstation': data.workstation,
'qty': pending_qty,
'pending_qty': pending_qty,
});
if (pending_qty) {
dialog.fields_dict.operations.df.data.push({
'name': data.name,
'operation': data.operation,
'workstation': data.workstation,
'batch_size': data.batch_size,
'qty': pending_qty,
'pending_qty': pending_qty
});
}
}
});
dialog.fields_dict.operations.grid.refresh();
@ -704,6 +716,8 @@ erpnext.work_order = {
stop_work_order: function(frm, status) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
freeze: true,
freeze_message: __("Updating Work Order status"),
args: {
work_order: frm.doc.name,
status: status

View File

@ -21,6 +21,12 @@
"produced_qty",
"sales_order",
"project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
@ -52,6 +58,7 @@
"actual_operating_cost",
"additional_operating_cost",
"column_break_24",
"corrective_operation_cost",
"total_operating_cost",
"more_info",
"description",
@ -488,6 +495,57 @@
"fieldtype": "Float",
"label": "Lead Time",
"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",
@ -495,7 +553,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2021-03-16 13:27:51.116484",
"modified": "2021-06-20 15:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"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
from __future__ import unicode_literals
import frappe
import json
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.utilities.transaction_base import validate_uom_is_integer
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 CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(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):
def onload(self):
@ -127,7 +125,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_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):
# already ordered qty
@ -235,12 +235,15 @@ class WorkOrder(Document):
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):
if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit"))
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
self.update_work_order_qty_in_combined_so()
else:
@ -260,12 +263,76 @@ class WorkOrder(Document):
self.update_work_order_qty_in_combined_so()
else:
self.update_work_order_qty_in_so()
self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
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):
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)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations):
self.set_operation_start_end_time(i, 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,
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()
for index, row in enumerate(self.operations):
qty = self.qty
while qty > 0:
qty = split_qty_based_on_batch_size(self, row, qty)
if row.job_card_qty > 0:
self.prepare_data_for_job_card(row, index,
plan_days, enable_capacity_planning)
planned_end_date = self.operations and self.operations[-1].planned_end_time
if 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):
"""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."""
@ -365,7 +440,7 @@ class WorkOrder(Document):
work_order_qty = qty[0][0] if qty and qty[0][0] else 0
frappe.db.set_value('Sales Order Item',
self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
def update_work_order_qty_in_combined_so(self):
total_bundle_qty = 1
if self.product_bundle_item:
@ -378,7 +453,7 @@ class WorkOrder(Document):
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')
for plan_reference in prod_plan.prod_plan_references:
work_order_qty = 0.0
if plan_reference.item_reference == item_reference:
@ -386,53 +461,54 @@ class WorkOrder(Document):
work_order_qty = flt(plan_reference.qty) / total_bundle_qty
frappe.db.set_value('Sales Order Item',
plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
def update_completed_qty_in_material_request(self):
if self.material_request:
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
def set_work_order_operations(self):
"""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:
return
if self.use_multi_level_bom:
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
operations = []
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:
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)
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()
def calculate_time(self):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
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()
@ -669,6 +745,17 @@ class WorkOrder(Document):
bom.set_bom_material_details()
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.validate_and_sanitize_search_inputs
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
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)
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.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()
@frappe.whitelist()
@ -867,13 +955,47 @@ def query_sales_order(production_item):
@frappe.whitelist()
def make_job_card(work_order, operations):
if isinstance(operations, string_types):
if isinstance(operations, str):
operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order)
for row in operations:
row = frappe._dict(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):
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.update({
'work_order': work_order.name,
'operation': row.get("operation"),
'workstation': row.get("workstation"),
'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"),
'bom_no': work_order.bom_no,
'project': work_order.project,
'company': work_order.company,
'sequence_id': row.get("sequence_id"),
'wip_warehouse': work_order.wip_warehouse
'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:

View File

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

View File

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

View File

@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
from __future__ import unicode_literals
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
import frappe
import unittest
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_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
class TestWorkstation(unittest.TestCase):
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 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,
"_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):
args = args if args else kwargs
if isinstance(args, tuple):
@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
"doctype": "Workstation",
"workstation_name": workstation_name
})
doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert()
return doc
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):
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:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %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):
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation)
if not workstation.working_hours:
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.add_doctype_to_sla #14-06-2021
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.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) {
@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
},
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;
let filters = {company: frm.doc.company};
if (frm.doc.type) {
filters.type = frm.doc.type;
}
frm.set_query("salary_component", function() {
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