Merge branch 'develop' into patch-4
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 60c614f..0c96d32 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
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'''
diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index 5e76403..c417a49 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -33,6 +33,8 @@
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 {}
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index dd346bc..2f86c6c 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -263,6 +263,9 @@
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)
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 7cd1e77..fac28c9 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -19,7 +19,7 @@
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)
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 1a6dbed..c6c6892 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -86,7 +86,7 @@
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 @@
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 {
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 948c513..11465b7 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -121,8 +121,7 @@
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))
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index b2e8626..a8c07d6 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -49,7 +49,15 @@
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]));
+ }
+ }
});
});
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 29dc96e..d76d909 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -216,7 +216,8 @@
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 @@
return accounts[0].name
-
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index d3ac3a6..439b1ed 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -7,6 +7,8 @@
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);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 54623dd..51f18a5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -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",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index b6b2bef..adaf99a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -706,7 +706,7 @@
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 @@
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):
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 617b5b4..7562418 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -27,10 +27,6 @@
});
}
- company() {
- erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
- }
-
onload() {
super.onload();
@@ -569,5 +565,9 @@
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
- }
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
})
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 45d89ad..c1cc092 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -517,6 +517,8 @@
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 @@
"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 @@
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)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ff433b9..ec93314 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -230,6 +230,27 @@
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 @@
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 @@
# 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()
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 29f7247..b39022d 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -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",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 114b7d2..fe531d3 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1957,6 +1957,33 @@
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 @@
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({
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 59009ae..25d2cf1 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -101,7 +101,7 @@
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
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 84f7868..4a551b8 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -36,16 +36,12 @@
{
"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 @@
"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 @@
"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")
+ });
}
},
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 562df4f..744ada9 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -49,8 +49,12 @@
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 @@
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_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 @@
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 = []
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
index 60e675f..48bd730 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
@@ -168,21 +168,24 @@
"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
+
}
]
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 630a1dc..b9c77d5 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -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",
diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md
new file mode 100644
index 0000000..d881b27
--- /dev/null
+++ b/erpnext/change_log/v13/v13_6_0.md
@@ -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))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 243939b..1c086e9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -828,8 +828,14 @@
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
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 7bd739a..2803193 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -19,7 +19,7 @@
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 @@
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,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 5f759b4..80ccc6d 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -99,9 +99,10 @@
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 @@
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
\ No newline at end of file
+ return serial_nos
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 7f28289..da2765d 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -330,9 +330,15 @@
# 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")
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 35097b9..8196cff 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -11,7 +11,7 @@
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 @@
})
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):
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
index 3f5c95a..fe5707a 100644
--- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
@@ -22,10 +22,10 @@
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 @@
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);
}
}
}
diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
index c93b788..33119d8 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
@@ -6,7 +6,7 @@
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 @@
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 @@
}).insert()
doc.submit()
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3da606b..ba10b58 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -158,6 +158,7 @@
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
+ {"from_route": "/project", "to_route": "Project"}
]
standard_portal_menu_items = [
diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js
index c3c3cb8..7964078 100644
--- a/erpnext/hr/doctype/attendance/attendance.js
+++ b/erpnext/hr/doctype/attendance/attendance.js
@@ -11,5 +11,5 @@
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
- }
+ }
}
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index f3b8a79..3412675 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -15,6 +15,7 @@
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 @@
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
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 0c7eafe..9a3bac0 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -21,6 +21,9 @@
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);
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 5442ed5..d592a9c 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -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",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index bc56942..fa017d9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -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.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 @@
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()
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 285374d..e853bee 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -7,7 +7,8 @@
'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 @@
},
{
'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
+ 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
},
{
'label': _('Shift'),
diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js
index 6679e31..d37e149 100644
--- a/erpnext/hr/doctype/employee/employee_list.js
+++ b/erpnext/hr/doctype/employee/employee_list.js
@@ -3,7 +3,7 @@
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;
}
};
diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/__init__.py
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
new file mode 100644
index 0000000..25c5bad
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
@@ -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
+ };
+ });
+ },
+});
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
new file mode 100644
index 0000000..5a91856
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
new file mode 100644
index 0000000..503b5ea
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
@@ -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"))
+ )
+
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
new file mode 100644
index 0000000..fc08e21
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
@@ -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];
+ }
+};
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
new file mode 100644
index 0000000..a615b20
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
@@ -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
+
diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/__init__.py
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js
new file mode 100644
index 0000000..425f2fd
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json
new file mode 100644
index 0000000..1dce00a
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.json
@@ -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"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py
new file mode 100644
index 0000000..618cf0a
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.py
@@ -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
diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
new file mode 100644
index 0000000..a02a34d
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
@@ -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
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
index 3b9141b..2ad0d59 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
@@ -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];
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index ae02c51..3a6539e 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -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"
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 11302ca..4757cd3 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -8,6 +8,7 @@
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 @@
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 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))
\ No newline at end of file
+ frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 6e7ae87..fdcd533 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -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 @@
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)
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js
index 04af232..228391b 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js
@@ -103,4 +103,4 @@
})
frm.set_value('total_estimated_budget', estimated_budget);
}
-}
\ No newline at end of file
+};
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 533149a..e6c783a 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -41,7 +41,7 @@
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 @@
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 @@
(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 @@
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 @@
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
\ No newline at end of file
+ return staffing_plan if staffing_plan else None
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json
index e49541e..f365003 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.json
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json
@@ -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",
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md
index 418fd49..b9ba846 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.md
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md
@@ -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>
\ No newline at end of file
+</table>
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 4dd4570..b8953b3 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -178,7 +178,7 @@
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))
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index c5201c2..4500ba4 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -154,6 +154,24 @@
"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,
"is_query_report": 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",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 44f841f..c566688 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -71,7 +71,6 @@
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 @@
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 @@
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);
});
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index f551b91..f38d1b9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d1f6385..c58f017 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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,
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 42b23f2..57a5458 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -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 @@
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 @@
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:
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 07464e3..4458e6d 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4e8dd41..81860c9 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -11,6 +11,16 @@
}
};
});
+
+ 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 @@
}
}
+ 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 @@
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 @@
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 @@
},
to_time: function(frm) {
- frm.set_value('job_started', 0);
frm.set_value('started_time', '');
}
})
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 5713f69..046e2fd 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -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",
@@ -118,13 +129,6 @@
"label": "Timing Detail"
},
{
- "fieldname": "employee",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Employee",
- "options": "Employee"
- },
- {
"allow_bulk_edit": 1,
"fieldname": "time_logs",
"fieldtype": "Table",
@@ -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",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index cdc4518..7f8f2ef 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -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 @@
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 @@
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 @@
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 @@
"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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index 100ef4c..d91530d 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
new file mode 100644
index 0000000..9a8692b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
new file mode 100644
index 0000000..85d7298
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
@@ -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
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index 9dd54dd..a7102d7 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index b7634da..024f784 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -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
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js
index 5c2aba6..102b678 100644
--- a/erpnext/manufacturing/doctype/operation/operation.js
+++ b/erpnext/manufacturing/doctype/operation/operation.js
@@ -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]]
+ }
+ };
+ });
}
-});
+});
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index c231fba..10a97ed 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -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
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py
index 69e8329..374f320 100644
--- a/erpnext/manufacturing/doctype/operation/operation.py
+++ b/erpnext/manufacturing/doctype/operation/operation.py
@@ -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
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 64d5841..450aa04 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -306,8 +306,25 @@
},
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) {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 46e0476..0ede1bd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -98,7 +98,7 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
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)
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 8312d74..ece0db7 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -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 @@
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
\ No newline at end of file
+ sequence_id = row.sequence_id
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 6a38dcf..92f2694 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -7,9 +7,7 @@
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 @@
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 @@
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 @@
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)
diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/__init__.py
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
new file mode 100644
index 0000000..be9db6a
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
new file mode 100644
index 0000000..f63d2b9
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
new file mode 100644
index 0000000..f4b2775
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
@@ -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
diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
new file mode 100644
index 0000000..d3410ca
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
@@ -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
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92..68de0b2 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@
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))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 3e5a72d..5120485 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -141,8 +141,7 @@
}
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 @@
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 @@
}, 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 @@
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 @@
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
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index cd9edee..44d76d2 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 2600790..180815d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
- 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)
+ 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
+
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 @@
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 @@
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 @@
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 @@
@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 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:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
index 87c090f..9aa0715 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
@@ -4,10 +4,17 @@
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']
}
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 8c5cde9..f7b8787 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -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
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c6699be..9b73aca 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -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 @@
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 @@
"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)
\ No newline at end of file
+ return frappe.get_doc("Workstation", workstation_name)
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 3512e59..f4483f7 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -39,7 +39,8 @@
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 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
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
new file mode 100644
index 0000000..97e7e0a
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -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
+ }
+ }
+ }
+ },
+ ]
+};
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
new file mode 100644
index 0000000..ee63bc1
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
new file mode 100644
index 0000000..9f81e7d
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -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"
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 95cdc30..986b0c5 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -288,4 +288,7 @@
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
diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
new file mode 100644
index 0000000..7de9fa1
--- /dev/null
+++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
@@ -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()
diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py
new file mode 100644
index 0000000..d4e65c6
--- /dev/null
+++ b/erpnext/patches/v13_0/update_job_card_details.py
@@ -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
+ """)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index d1ed91f..24ffce5 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,8 +12,12 @@
}
};
});
+ },
- frm.trigger('set_earning_component');
+ onload: function(frm) {
+ if (frm.doc.type) {
+ frm.trigger('set_component_query');
+ }
},
employee: function(frm) {
@@ -46,14 +50,19 @@
},
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
};
});
},
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index f289260..496c37b 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -135,10 +135,26 @@
});
frm.set_query('employee', 'employees', () => {
- if (!frm.doc.company) {
- frappe.msgprint(__("Please set a Company"));
- return [];
+ let error_fields = [];
+ let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
+
+ let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
+
+ mandatory_fields.forEach(field => {
+ if (!frm.doc[field]) {
+ error_fields.push(frappe.unscrub(field));
+ }
+ });
+
+ if (error_fields && error_fields.length) {
+ message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
+ frappe.throw({
+ message: message,
+ indicator: 'red',
+ title: __('Missing Fields')
+ });
}
+
return {
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
filters: frm.events.get_employee_filters(frm)
@@ -148,25 +164,22 @@
get_employee_filters: function (frm) {
let filters = {};
- filters['company'] = frm.doc.company;
- filters['start_date'] = frm.doc.start_date;
- filters['end_date'] = frm.doc.end_date;
filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
- filters['payroll_frequency'] = frm.doc.payroll_frequency;
- filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
- filters['currency'] = frm.doc.currency;
- if (frm.doc.department) {
- filters['department'] = frm.doc.department;
- }
- if (frm.doc.branch) {
- filters['branch'] = frm.doc.branch;
- }
- if (frm.doc.designation) {
- filters['designation'] = frm.doc.designation;
- }
+ let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
+ 'currency', 'department', 'branch', 'designation'];
+
+ fields.forEach(field => {
+ if (frm.doc[field]) {
+ filters[field] = frm.doc[field];
+ }
+ });
+
if (frm.doc.employees) {
- filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
+ let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
+ if (employees && employees.length) {
+ filters['employees'] = employees;
+ }
}
return filters;
},
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 7a70679..36e728f 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -11,6 +11,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from frappe.desk.reportview import get_match_cond, get_filters_cond
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class PayrollEntry(Document):
def onload(self):
@@ -41,7 +42,7 @@
emp_with_sal_slip.append(employee_details.employee)
if len(emp_with_sal_slip):
- frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
+ frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
@@ -211,7 +212,7 @@
return account_dict
def make_accrual_jv_entry(self):
- self.check_permission('write')
+ self.check_permission("write")
earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {}
payroll_payable_account = self.payroll_payable_account
@@ -219,12 +220,13 @@
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
if earnings or deductions:
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Journal Entry'
- journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Journal Entry"
+ journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\
.format(self.start_date, self.end_date)
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
+ accounting_dimensions = get_accounting_dimensions() or []
accounts = []
currencies = []
@@ -236,37 +238,34 @@
for acc_cc, amount in earnings.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount += flt(amount, precision)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0],
"debit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate),
- "party_type": '',
"cost_center": acc_cc[1] or self.cost_center,
"project": self.project
- })
+ }, accounting_dimensions))
# Deductions
for acc_cc, amount in deductions.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount -= flt(amount, precision)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0],
"credit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate),
"cost_center": acc_cc[1] or self.cost_center,
- "party_type": '',
"project": self.project
- })
+ }, accounting_dimensions))
# Payable amount
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": payroll_payable_account,
"credit_in_account_currency": flt(payable_amt, precision),
"exchange_rate": flt(exchange_rate),
- "party_type": '',
"cost_center": self.cost_center
- })
+ }, accounting_dimensions))
journal_entry.set("accounts", accounts)
if len(currencies) > 1:
@@ -286,6 +285,12 @@
return jv_name
+ def update_accounting_dimensions(self, row, accounting_dimensions):
+ for dimension in accounting_dimensions:
+ row.update({dimension: self.get(dimension)})
+
+ return row
+
def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies):
conversion_rate = 1
exchange_rate = self.exchange_rate
@@ -454,6 +459,7 @@
where
t1.name = t2.employee
and t2.docstatus = 1
+ and t1.status != 'Inactive'
%s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
@@ -674,6 +680,10 @@
conditions = []
include_employees = []
emp_cond = ''
+
+ if not filters.payroll_frequency:
+ frappe.throw(_('Select Payroll Frequency.'))
+
if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters)
emp = filters.get('employees')
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 9e7db97..ce88cc3 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -481,6 +481,7 @@
if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
+
employee = frappe.db.get_value("Employee", {"user_id": user})
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index dce6b7a..e7d123c 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -124,8 +124,8 @@
"doctype": "Salary Structure",
"name": salary_structure,
"company": company or erpnext.get_default_company(),
- "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
- "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
+ "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
+ "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
index 0a5ebef..8aa0734 100644
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ b/erpnext/portal/product_configurator/test_product_configurator.py
@@ -41,6 +41,30 @@
"show_variant_in_website": 1
}).insert()
+ def create_regular_web_item(self, name, item_group=None):
+ if not frappe.db.exists('Item', name):
+ doc = frappe.get_doc({
+ "description": name,
+ "item_code": name,
+ "item_name": name,
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_group": item_group or "_Test Item Group",
+ "stock_uom": "_Test UOM",
+ "item_defaults": [{
+ "company": "_Test Company",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "buying_cost_center": "_Test Cost Center - _TC",
+ "selling_cost_center": "_Test Cost Center - _TC",
+ "income_account": "Sales - _TC"
+ }],
+ "show_in_website": 1
+ }).insert()
+ else:
+ doc = frappe.get_doc("Item", name)
+ return doc
+
def test_product_list(self):
template_items = frappe.get_all('Item', {'show_in_website': 1})
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
@@ -77,3 +101,42 @@
'Test Size': ['2XL']
})
self.assertEqual(len(items), 1)
+
+ def test_products_in_multiple_item_groups(self):
+ """Check if product is visible on multiple item group pages barring its own."""
+ from erpnext.shopping_cart.product_query import ProductQuery
+
+ if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
+ item_group_doc = frappe.get_doc({
+ "doctype": "Item Group",
+ "item_group_name": "Tech Items",
+ "parent_item_group": "All Item Groups",
+ "show_in_website": 1
+ }).insert()
+ else:
+ item_group_doc = frappe.get_doc("Item Group", "Tech Items")
+
+ doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
+ if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
+ doc.append("website_item_groups", {
+ "item_group": "_Test Item Group Desktops"
+ })
+ doc.save()
+
+ # check if item is visible in its own Item Group's page
+ engine = ProductQuery()
+ items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].item_code, "Portal Item")
+
+ # check if item is visible in configured foreign Item Group's page
+ engine = ProductQuery()
+ items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
+ item_codes = [row.item_code for row in items]
+
+ self.assertIn(len(items), [2, 3])
+ self.assertIn("Portal Item", item_codes)
+
+ # teardown
+ doc.delete()
+ item_group_doc.delete()
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index cc33b8b..0471704 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -272,11 +272,14 @@
let me = this;
let item_codes = [];
let item_rates = {};
+ let item_tax_templates = {};
+
$.each(this.frm.doc.items || [], function(i, item) {
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.net_rate;
+ item_tax_templates[item.name] = item.item_tax_template;
}
});
@@ -287,18 +290,16 @@
company: me.frm.doc.company,
tax_category: cstr(me.frm.doc.tax_category),
item_codes: item_codes,
- item_rates: item_rates
+ item_rates: item_rates,
+ item_tax_templates: item_tax_templates
},
callback: function(r) {
if (!r.exc) {
$.each(me.frm.doc.items || [], function(i, item) {
- if (item.name && r.message.hasOwnProperty(item.name)) {
+ if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
item.item_tax_template = r.message[item.name].item_tax_template;
item.item_tax_rate = r.message[item.name].item_tax_rate;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
- } else {
- item.item_tax_template = "";
- item.item_tax_rate = "{}";
}
});
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 210237f..8360337 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -888,9 +888,6 @@
}
- if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date;
- else var date = this.frm.doc.transaction_date;
-
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 808dd5a..a79eadc 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -274,9 +274,9 @@
return true;
}
-erpnext.utils.get_shipping_address = function(frm, callback){
+erpnext.utils.get_shipping_address = function(frm, callback) {
if (frm.doc.company) {
- if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference ||
+ if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference ||
frm.doc.internal_order_reference)) {
if (callback) {
return callback();
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 9402cf9..5962859 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -467,11 +467,15 @@
.btn-change-address {
color: var(--blue-500);
- box-shadow: none;
- border: 1px solid var(--blue-500);
}
}
+.btn-new-address:hover, .btn-change-address:hover {
+ box-shadow: none;
+ color: var(--blue-500) !important;
+ border: 1px solid var(--blue-500);
+}
+
.modal .address-card {
.card-body {
padding: var(--padding-sm);
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 11ebef7..5d33c1b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -385,13 +385,16 @@
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
- if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
+ if abs(
+ flt(value_details['TotInvVal']) + flt(value_details['Discount']) -
+ flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) -
+ total_item_value) > 1:
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
calculated_invoice_value = \
flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
- + flt(value_details['OthChrg']) - flt(value_details['Discount'])
+ + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount'])
if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 80e2d72..1096159 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -201,7 +201,7 @@
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
- conditions += " AND billing_address_gstin NOT IN %(company_gstins)s"
+ conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s"
return conditions
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py
index d3281f7..ae3482f 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -4,6 +4,8 @@
from __future__ import unicode_literals
import frappe
+from frappe.utils import get_link_to_form
+
from frappe import _
from frappe.model.document import Document
@@ -18,6 +20,27 @@
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty")
+ def on_trash(self):
+ linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice",
+ "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"]
+
+ invoice_links = []
+ for doctype in linked_doctypes:
+ item_doctype = doctype + " Item"
+
+ if doctype == "Stock Entry":
+ item_doctype = doctype + " Detail"
+
+ invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"])
+
+ for invoice in invoices:
+ invoice_links.append(get_link_to_form(doctype, invoice['parent']))
+
+ if len(invoice_links):
+ frappe.throw(
+ "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
+ .format(", ".join(invoice_links)), title=_("Not Allowed"))
+
def validate_main_item(self):
"""Validates, main Item is not a stock item"""
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 551f715..41f57a3 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -233,7 +233,7 @@
# Checks Sales Invoice
submit_rv = frappe.db.sql_list("""select t1.name
from `tabSales Invoice` t1,`tabSales Invoice Item` t2
- where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""",
+ where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""",
self.name)
if submit_rv:
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 9873710..974648d 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1217,6 +1217,19 @@
# To test if the SO does NOT have a Blanket Order
self.assertEqual(so_doc.items[0].blanket_order, None)
+ def test_so_cancellation_when_si_drafted(self):
+ """
+ Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state
+ Expected result: sales order should not get cancelled
+ """
+ so = make_sales_order()
+ so.submit()
+ si = make_sales_invoice(so.name)
+ si.save()
+
+ self.assertRaises(frappe.ValidationError, so.cancel)
+
+
def make_sales_order(**args):
so = frappe.new_doc("Sales Order")
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ae3f9e3..c827368 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -241,8 +241,8 @@
events: {
get_frm: () => this.frm,
- cart_item_clicked: (item_code, batch_no, uom, rate) => {
- const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
+ cart_item_clicked: (item) => {
+ const item_row = this.get_item_from_frm(item);
this.item_details.toggle_item_details_section(item_row);
},
@@ -273,17 +273,15 @@
this.cart.toggle_numpad(minimize);
},
- form_updated: (cdt, cdn, fieldname, value) => {
- const item_row = frappe.model.get_doc(cdt, cdn);
- if (item_row && item_row[fieldname] != value) {
-
- const { item_code, batch_no, uom, rate } = this.item_details.current_item;
- const event = {
- field: fieldname,
+ form_updated: (item, field, value) => {
+ const item_row = frappe.model.get_doc(item.doctype, item.name);
+ if (item_row && item_row[field] != value) {
+ const args = {
+ field,
value,
- item: { item_code, batch_no, uom, rate }
- }
- return this.on_cart_update(event)
+ item: this.item_details.current_item
+ };
+ return this.on_cart_update(args);
}
return Promise.resolve();
@@ -300,19 +298,18 @@
set_value_in_current_cart_item: (selector, value) => {
this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
},
- clone_new_batch_item_in_frm: (batch_serial_map, current_item) => {
+ clone_new_batch_item_in_frm: (batch_serial_map, item) => {
// called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
// for each unique batch new item row is added in the form & cart
Object.keys(batch_serial_map).forEach(batch => {
- const { item_code, batch_no } = current_item;
- const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
+ const item_to_clone = this.frm.doc.items.find(i => i.name == item.name);
const new_row = this.frm.add_child("items", { ...item_to_clone });
// update new serialno and batch
new_row.batch_no = batch;
new_row.serial_no = batch_serial_map[batch].join(`\n`);
new_row.qty = batch_serial_map[batch].length;
this.frm.doc.items.forEach(row => {
- if (item_code === row.item_code) {
+ if (item.item_code === row.item_code) {
this.update_cart_html(row);
}
});
@@ -321,8 +318,8 @@
remove_item_from_cart: () => this.remove_item_from_cart(),
get_item_stock_map: () => this.item_stock_map,
close_item_details: () => {
- this.item_details.toggle_item_details_section(undefined);
- this.cart.prev_action = undefined;
+ this.item_details.toggle_item_details_section(null);
+ this.cart.prev_action = null;
this.cart.toggle_item_highlight();
},
get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
@@ -506,50 +503,47 @@
let item_row = undefined;
try {
let { field, value, item } = args;
- const { item_code, batch_no, serial_no, uom, rate } = item;
- item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
+ item_row = this.get_item_from_frm(item);
+ const item_row_exists = !$.isEmptyObject(item_row);
- const item_selected_from_selector = field === 'qty' && value === "+1"
+ const from_selector = field === 'qty' && value === "+1";
+ if (from_selector)
+ value = flt(item_row.qty) + flt(value);
- if (item_row) {
- item_selected_from_selector && (value = item_row.qty + flt(value))
-
- field === 'qty' && (value = flt(value));
+ if (item_row_exists) {
+ if (field === 'qty')
+ value = flt(value);
if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) {
const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
- if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
+ if (this.is_current_item_being_edited(item_row) || from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
this.update_cart_html(item_row);
}
} else {
- if (!this.frm.doc.customer) {
- frappe.dom.unfreeze();
- frappe.show_alert({
- message: __('You must select a customer before adding an item.'),
- indicator: 'orange'
- });
- frappe.utils.play_sound("error");
+ if (!this.frm.doc.customer)
+ return this.raise_customer_selection_alert();
+
+ const { item_code, batch_no, serial_no, rate } = item;
+
+ if (!item_code)
return;
- }
- if (!item_code) return;
- item_selected_from_selector && (value = flt(value))
-
- const args = { item_code, batch_no, rate, [field]: value };
+ const new_item = { item_code, batch_no, rate, [field]: value };
if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
- args['serial_no'] = serial_no;
+ new_item['serial_no'] = serial_no;
}
- if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0;
+ if (field === 'serial_no')
+ new_item['qty'] = value.split(`\n`).length || 0;
- item_row = this.frm.add_child('items', args);
+ item_row = this.frm.add_child('items', new_item);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
@@ -558,8 +552,11 @@
this.update_cart_html(item_row);
- this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
- this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
+ if (this.item_details.$component.is(':visible'))
+ this.edit_item_details_of(item_row);
+
+ if (this.check_serial_batch_selection_needed(item_row))
+ this.edit_item_details_of(item_row);
}
} catch (error) {
@@ -570,14 +567,33 @@
}
}
- get_item_from_frm(item_code, batch_no, uom, rate) {
- const has_batch_no = batch_no;
- return this.frm.doc.items.find(
- i => i.item_code === item_code
- && (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
- && (i.uom === uom)
- && (i.rate == rate)
- );
+ raise_customer_selection_alert() {
+ frappe.dom.unfreeze();
+ frappe.show_alert({
+ message: __('You must select a customer before adding an item.'),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ }
+
+ get_item_from_frm({ name, item_code, batch_no, uom, rate }) {
+ let item_row = null;
+ if (name) {
+ item_row = this.frm.doc.items.find(i => i.name == name);
+ } else {
+ // if item is clicked twice from item selector
+ // then "item_code, batch_no, uom, rate" will help in getting the exact item
+ // to increase the qty by one
+ const has_batch_no = batch_no;
+ item_row = this.frm.doc.items.find(
+ i => i.item_code === item_code
+ && (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
+ && (i.uom === uom)
+ && (i.rate == rate)
+ );
+ }
+
+ return item_row || {};
}
edit_item_details_of(item_row) {
@@ -585,9 +601,7 @@
}
is_current_item_being_edited(item_row) {
- const { item_code, batch_no } = this.item_details.current_item;
-
- return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
+ return item_row.name == this.item_details.current_item.name;
}
update_cart_html(item_row, remove_item) {
@@ -669,7 +683,7 @@
update_item_field(value, field_or_action) {
if (field_or_action === 'checkout') {
- this.item_details.toggle_item_details_section(undefined);
+ this.item_details.toggle_item_details_section(null);
} else if (field_or_action === 'remove') {
this.remove_item_from_cart();
} else {
@@ -688,7 +702,7 @@
.then(() => {
frappe.model.clear_doc(doctype, name);
this.update_cart_html(current_item, true);
- this.item_details.toggle_item_details_section(undefined);
+ this.item_details.toggle_item_details_section(null);
frappe.dom.unfreeze();
})
.catch(e => console.log(e));
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index f5019f5..7cae0e4 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -181,11 +181,8 @@
me.$totals_section.find(".edit-cart-btn").click();
}
- const item_code = unescape($cart_item.attr('data-item-code'));
- const batch_no = unescape($cart_item.attr('data-batch-no'));
- const uom = unescape($cart_item.attr('data-uom'));
- const rate = unescape($cart_item.attr('data-rate'));
- me.events.cart_item_clicked(item_code, batch_no, uom, rate);
+ const item_row_name = unescape($cart_item.attr('data-row-name'));
+ me.events.cart_item_clicked({ name: item_row_name });
this.numpad_value = '';
});
@@ -521,25 +518,14 @@
}
}
- get_cart_item({ item_code, batch_no, uom, rate }) {
- const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
- const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
- const uom_attr = `[data-uom="${escape(uom)}"]`;
- const rate_attr = `[data-rate="${escape(rate)}"]`;
-
- const item_selector = batch_no ?
- `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
-
+ get_cart_item({ name }) {
+ const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
return this.$cart_items_wrapper.find(item_selector);
}
get_item_from_frm(item) {
const doc = this.events.get_frm().doc;
- const { item_code, batch_no, uom, rate } = item;
- const search_field = batch_no ? 'batch_no' : 'item_code';
- const search_value = batch_no || item_code;
-
- return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
+ return doc.items.find(i => i.name == item.name);
}
update_item_html(item, remove_item) {
@@ -564,10 +550,7 @@
if (!$item_to_update.length) {
this.$cart_items_wrapper.append(
- `<div class="cart-item-wrapper"
- data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
- data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
- </div>
+ `<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
<div class="seperator"></div>`
)
$item_to_update = this.get_cart_item(item_data);
@@ -642,7 +625,7 @@
function get_item_image_html() {
const { image, item_name } = item_data;
- if (image) {
+ if (!me.hide_images && image) {
return `
<div class="item-image">
<img
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index 5e09df8..6a4d3d5 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -2,6 +2,7 @@
constructor({ wrapper, events, settings }) {
this.wrapper = wrapper;
this.events = events;
+ this.hide_images = settings.hide_images;
this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change;
this.current_item = {};
@@ -54,36 +55,28 @@
this.$dicount_section = this.$component.find('.discount-section');
}
- has_item_has_changed(item) {
- const { item_code, batch_no, uom, rate } = this.current_item;
- const item_code_is_same = item && item_code === item.item_code;
- const batch_is_same = item && batch_no == item.batch_no;
- const uom_is_same = item && uom === item.uom;
- const rate_is_same = item && rate === item.rate;
-
- if (!item)
- return false;
-
- if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
- return false;
-
- return true;
+ compare_with_current_item(item) {
+ // returns true if `item` is currently being edited
+ return item && item.name == this.current_item.name;
}
toggle_item_details_section(item) {
- this.item_has_changed = this.has_item_has_changed(item);
+ const current_item_changed = !this.compare_with_current_item(item);
- this.events.toggle_item_selector(this.item_has_changed);
- this.toggle_component(this.item_has_changed);
+ // if item is null or highlighted cart item is clicked twice
+ const hide_item_details = !Boolean(item) || !current_item_changed;
+
+ this.events.toggle_item_selector(!hide_item_details);
+ this.toggle_component(!hide_item_details);
- if (this.item_has_changed) {
+ if (item && current_item_changed) {
this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name;
this.item_row = item;
this.currency = this.events.get_frm().doc.currency;
- this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate };
+ this.current_item = item;
this.render_dom(item);
this.render_discount_dom(item);
@@ -132,7 +125,7 @@
this.$item_name.html(item_name);
this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency));
- if (image) {
+ if (!this.hide_images && image) {
this.$item_image.html(
`<img
onerror="cur_pos.item_details.handle_broken_image(this)"
@@ -180,7 +173,7 @@
df: {
...field_meta,
onchange: function() {
- me.events.form_updated(me.doctype, me.name, fieldname, this.value);
+ me.events.form_updated(me.current_item, fieldname, this.value);
}
},
parent: this.$form_container.find(`.${fieldname}-control`),
@@ -218,22 +211,17 @@
bind_custom_control_change_event() {
const me = this;
if (this.rate_control) {
- if (this.allow_rate_change) {
- this.rate_control.df.onchange = function() {
- if (this.value || flt(this.value) === 0) {
- me.events.set_value_in_current_cart_item('rate', this.value);
- me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
- const item_row = frappe.get_doc(me.doctype, me.name);
- const doc = me.events.get_frm().doc;
- me.$item_price.html(format_currency(item_row.rate, doc.currency));
- me.render_discount_dom(item_row);
- });
- me.current_item.rate = this.value;
- }
- };
- } else {
- this.rate_control.df.read_only = 1;
- }
+ this.rate_control.df.onchange = function() {
+ if (this.value || flt(this.value) === 0) {
+ me.events.form_updated(me.current_item, 'rate', this.value).then(() => {
+ const item_row = frappe.get_doc(me.doctype, me.name);
+ const doc = me.events.get_frm().doc;
+ me.$item_price.html(format_currency(item_row.rate, doc.currency));
+ me.render_discount_dom(item_row);
+ });
+ }
+ };
+ this.rate_control.df.read_only = !this.allow_rate_change;
this.rate_control.refresh();
}
@@ -246,7 +234,7 @@
this.warehouse_control.df.reqd = 1;
this.warehouse_control.df.onchange = function() {
if (this.value) {
- me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => {
+ me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map();
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
if (available_qty === undefined) {
@@ -278,7 +266,7 @@
this.serial_no_control.df.reqd = 1;
this.serial_no_control.df.onchange = async function() {
!me.current_item.batch_no && await me.auto_update_batch_no();
- me.events.form_updated(me.doctype, me.name, 'serial_no', this.value);
+ me.events.form_updated(me.current_item, 'serial_no', this.value);
}
this.serial_no_control.refresh();
}
@@ -295,19 +283,12 @@
}
}
};
- this.batch_no_control.df.onchange = function() {
- me.events.set_value_in_current_cart_item('batch-no', this.value);
- me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
- me.current_item.batch_no = this.value;
- }
this.batch_no_control.refresh();
}
if (this.uom_control) {
this.uom_control.df.onchange = function() {
- me.events.set_value_in_current_cart_item('uom', this.value);
- me.events.form_updated(me.doctype, me.name, 'uom', this.value);
- me.current_item.uom = this.value;
+ me.events.form_updated(me.current_item, 'uom', this.value);
const item_row = frappe.get_doc(me.doctype, me.name);
me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value);
@@ -317,9 +298,9 @@
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`];
- const item_is_same = !this.has_item_has_changed(item_row);
+ const item_row_is_being_edited = this.compare_with_current_item(item_row);
- if (item_is_same && field_control && field_control.get_value() !== value) {
+ if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
field_control.set_value(value);
cur_pos.update_cart_html(item_row);
}
@@ -337,7 +318,9 @@
fields: ["batch_no", "name"]
});
const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
- acc[r.batch_no] || (acc[r.batch_no] = []);
+ if (!acc[r.batch_no]) {
+ acc[r.batch_no] = [];
+ }
acc[r.batch_no] = [...acc[r.batch_no], r.name];
return acc;
}, {});
@@ -353,12 +336,10 @@
if (serial_nos_belongs_to_other_batch) {
this.serial_no_control.set_value(batch_serial_nos);
this.qty_control.set_value(batch_serial_map[batch_no].length);
- }
- delete batch_serial_map[batch_no];
-
- if (serial_nos_belongs_to_other_batch)
+ delete batch_serial_map[batch_no];
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
+ }
}
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 64c529e..dd7f143 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -232,7 +232,11 @@
uom = uom === "undefined" ? undefined : uom;
rate = rate === "undefined" ? undefined : rate;
- me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }});
+ me.events.item_selected({
+ field: 'qty',
+ value: "+1",
+ item: { item_code, batch_no, serial_no, uom, rate }
+ });
me.set_search_value('');
});
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index f5feb95..8cb2446 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -59,7 +59,7 @@
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty,
- IFNULL(sii.qty, 0) as billed_qty,
+ IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 27e023c..0427abe 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -407,8 +407,6 @@
frappe.only_for("System Manager")
- frappe.db.set_value("Company", company, "abbr", new)
-
def _rename_record(doc):
parts = doc[0].rsplit(" - ", 1)
if len(parts) == 1 or parts[1].lower() == old.lower():
@@ -419,11 +417,18 @@
doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company))
for d in doc:
_rename_record(d)
+ try:
+ frappe.db.auto_commit_on_many_writes = 1
+ frappe.db.set_value("Company", company, "abbr", new)
+ for dt in ["Warehouse", "Account", "Cost Center", "Department",
+ "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
+ _rename_records(dt)
+ frappe.db.commit()
- for dt in ["Warehouse", "Account", "Cost Center", "Department",
- "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
- _rename_records(dt)
- frappe.db.commit()
+ except Exception:
+ frappe.log_error(title=_('Abbreviation Rename Error'))
+ finally:
+ frappe.db.auto_commit_on_many_writes = 0
def get_name_with_abbr(name, company):
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index db480e0..1a83cb6 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -91,7 +91,7 @@
field_filters['item_group'] = self.name
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start)
+ context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
filter_engine = ProductFiltersBuilder(self.name)
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py
index 6c63d87..7dfa09e 100644
--- a/erpnext/shopping_cart/filters.py
+++ b/erpnext/shopping_cart/filters.py
@@ -22,12 +22,15 @@
filter_data = []
for df in fields:
- filters = {}
+ filters, or_filters = {}, []
if df.fieldtype == "Link":
if self.item_group:
- filters['item_group'] = self.item_group
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+ values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
else:
doctype = df.get_link_doctype()
@@ -44,7 +47,9 @@
values = [d.name for d in frappe.get_all(doctype, filters)]
# Remove None
- values = values.remove(None) if None in values else values
+ if None in values:
+ values.remove(None)
+
if values:
filter_data.append([df, values])
@@ -61,14 +66,18 @@
for attr_doc in attribute_docs:
selected_attributes = []
for attr in attr_doc.item_attribute_values:
+ or_filters = []
filters= [
["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
]
if self.item_group:
- filters.append(["item_group", "=", self.item_group])
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- if frappe.db.get_all("Item", filters, limit=1):
+ if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
if selected_attributes:
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 36d446e..3eab4ff 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -22,13 +22,14 @@
self.settings = frappe.get_doc("Products Settings")
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20
- self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
+ self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
+ 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
self.filters = []
self.or_filters = [['show_in_website', '=', 1]]
if not self.settings.get('hide_variants'):
self.or_filters.append(['show_variant_in_website', '=', 1])
- def query(self, attributes=None, fields=None, search_term=None, start=0):
+ def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary
Args:
@@ -44,6 +45,15 @@
if search_term: self.build_search_filters(search_term)
result = []
+ website_item_groups = []
+
+ # if from item group page consider website item group table
+ if item_group:
+ website_item_groups = frappe.db.get_all(
+ "Item",
+ fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
+ filters=[["Website Item Group", "item_group", "=", item_group]]
+ )
if attributes:
all_items = []
@@ -61,22 +71,38 @@
],
or_filters=self.or_filters,
start=start,
- limit=self.page_length
+ limit=self.page_length,
+ order_by="weightage desc"
)
items_dict = {item.name: item for item in items}
- # TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
else:
- result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length)
+ result = frappe.get_all(
+ "Item",
+ fields=self.fields,
+ filters=self.filters,
+ or_filters=self.or_filters,
+ start=start,
+ limit=self.page_length
+ )
+
+ # Combine results having context of website item groups into item results
+ if item_group and website_item_groups:
+ items_list = {row.name for row in result}
+ for row in website_item_groups:
+ if row.wig_parent not in items_list:
+ result.append(row)
+
+ result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
- item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
+ item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
return result
@@ -90,7 +116,16 @@
if not values:
continue
- if isinstance(values, list):
+ # handle multiselect fields in filter addition
+ meta = frappe.get_meta('Item', cached=True)
+ df = meta.get_field(field)
+ if df.fieldtype == 'Table MultiSelect':
+ child_doctype = df.options
+ child_meta = frappe.get_meta(child_doctype, cached=True)
+ fields = child_meta.get("fields")
+ if fields:
+ self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
+ elif isinstance(values, list):
# If value is a list use `IN` query
self.filters.append([field, 'IN', values])
else:
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index 943cb34..e6d2e13 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "field:batch_id",
"creation": "2013-03-05 14:50:38",
@@ -25,7 +26,11 @@
"reference_doctype",
"reference_name",
"section_break_7",
- "description"
+ "description",
+ "manufacturing_section",
+ "qty_to_produce",
+ "column_break_23",
+ "produced_qty"
],
"fields": [
{
@@ -160,13 +165,35 @@
"label": "Batch UOM",
"options": "UOM",
"read_only": 1
+ },
+ {
+ "fieldname": "manufacturing_section",
+ "fieldtype": "Section Break",
+ "label": "Manufacturing"
+ },
+ {
+ "fieldname": "qty_to_produce",
+ "fieldtype": "Float",
+ "label": "Qty To Produce",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "produced_qty",
+ "fieldtype": "Float",
+ "label": "Produced Qty",
+ "read_only": 1
}
],
"icon": "fa fa-archive",
"idx": 1,
"image_field": "image",
+ "links": [],
"max_attachments": 5,
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-01-07 11:10:09.149170",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 508e17c..b6eef6c 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -226,13 +226,12 @@
return batch.name
-def set_batch_nos(doc, warehouse_field, throw=False):
+def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
"""Automatically select `batch_no` for outgoing items in item table"""
- for d in doc.items:
+ for d in doc.get(child_table):
qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0
- has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
warehouse = d.get(warehouse_field, None)
- if has_batch_no and warehouse and qty > 0:
+ if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'):
if not d.batch_no:
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
else:
@@ -308,4 +307,9 @@
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
frappe.throw(_("There is no batch found against the {0}: {1}")
- .format(message, serial_no_link))
\ No newline at end of file
+ .format(message, serial_no_link))
+
+def make_batch(args):
+ if frappe.db.get_value("Item", args.item, "has_batch_no"):
+ args.doctype = "Batch"
+ frappe.get_doc(args).insert().name
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index c3803f1..36dfa6d 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -78,6 +78,9 @@
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ frm.set_df_property('packed_items', 'cannot_add_rows', true);
+ frm.set_df_property('packed_items', 'cannot_delete_rows', true);
},
print_without_amount: function(frm) {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 280fde1..f20e76f 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -554,8 +554,7 @@
"oldfieldname": "packing_details",
"oldfieldtype": "Table",
"options": "Packed Item",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "product_bundle_help",
@@ -1289,7 +1288,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-15 23:55:49.620641",
+ "modified": "2021-06-11 19:27:30.901112",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index dd31965..4808e94 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -129,12 +129,13 @@
self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc()
- if self._action != 'submit' and not self.is_return:
- set_batch_nos(self, 'warehouse', True)
-
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
+ if self._action != 'submit' and not self.is_return:
+ set_batch_nos(self, 'warehouse', throw=True)
+ set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items")
+
self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed'
@@ -181,9 +182,8 @@
super(DeliveryNote, self).validate_warehouse()
for d in self.get_item_list():
- if frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1:
- if not d['warehouse']:
- frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
+ if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1:
+ frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
def update_current_stock(self):
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0c63df0..f981aeb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -7,7 +7,7 @@
import frappe
import json
import frappe.defaults
-from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
+from frappe.utils import nowdate, nowtime, cstr, flt
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
@@ -18,9 +18,11 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
import create_stock_reconciliation, set_valuation_method
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
-from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
+from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
-from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self):
@@ -277,8 +279,6 @@
dn.cancel()
def test_sales_return_for_non_bundled_items_full(self):
- from erpnext.stock.doctype.item.test_item import make_item
-
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
make_item("Box", {'is_stock_item': 1})
@@ -741,6 +741,25 @@
self.assertEqual(si2.items[0].qty, 2)
self.assertEqual(si2.items[1].qty, 1)
+
+ def test_delivery_note_bundle_with_batched_item(self):
+ batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0})
+ batched_item = make_item("_Test Batched Item",
+ {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"}
+ )
+ make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
+ make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42)
+
+ try:
+ dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
+ except frappe.ValidationError as e:
+ if "batch" in str(e).lower():
+ self.fail("Batch numbers not getting added to bundled items in DN.")
+ raise e
+
+ self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
+
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 6585e1c..5f53be0 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -101,7 +101,8 @@
}
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
- if (flt(frm.doc.per_ordered, 2) < 100) {
+ let precision = frappe.defaults.get_default("float_precision");
+ if (flt(frm.doc.per_ordered, precision) < 100) {
let add_create_pick_list_button = () => {
frm.add_custom_button(__('Pick List'),
() => frm.events.create_pick_list(frm), __('Create'));
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 335175f..3ad9909 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -189,7 +189,7 @@
item_wh_list = []
for d in self.get("items"):
if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \
- and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and d.warehouse:
+ and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 :
item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index c01388d..2146793 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -184,4 +184,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 6ab68e2..e795742 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -17,6 +17,9 @@
# TODO: Prioritize SO or WO group warehouse
class PickList(Document):
+ def validate(self):
+ self.validate_for_qty()
+
def before_save(self):
self.set_item_locations()
@@ -35,6 +38,7 @@
@frappe.whitelist()
def set_item_locations(self, save=False):
+ self.validate_for_qty()
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@@ -107,6 +111,11 @@
return item_map.values()
+ def validate_for_qty(self):
+ if self.purpose == "Material Transfer for Manufacture" \
+ and (self.for_qty is None or self.for_qty == 0):
+ frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
+
def validate_item_locations(pick_list):
if not pick_list.locations:
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index c4da05a..84566b8 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -37,6 +37,7 @@
'company': '_Test Company',
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item',
'qty': 5,
@@ -90,6 +91,7 @@
'company': '_Test Company',
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item Warehouse Group Wise Reorder',
'qty': 1000,
@@ -135,6 +137,7 @@
'company': '_Test Company',
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Serialized Item',
'qty': 1000,
@@ -264,6 +267,7 @@
'company': '_Test Company',
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item',
'qty': 5,
@@ -319,6 +323,7 @@
'company': '_Test Company',
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item',
'qty': 1,
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index b8580f9..5ba9c70 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -254,6 +254,8 @@
return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
+ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_purchase_document_details
+
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -262,6 +264,8 @@
warehouse_with_no_account = []
stock_items = self.get_stock_items()
+ exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+
for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
@@ -287,7 +291,7 @@
continue
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
- stock_rbnb, account_currency=warehouse_account_currency, item=d)
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -304,6 +308,23 @@
-1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
+ # check if the exchange rate has changed
+ if d.get('purchase_invoice'):
+ if exchange_rate_map[d.purchase_invoice] and \
+ self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \
+ d.net_rate == net_rate_map[d.purchase_invoice_item]:
+
+ discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \
+ (exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
+
+ self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=credit_currency, item=d)
+
+ self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=credit_currency, item=d)
+
# Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
@@ -581,7 +602,6 @@
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
- from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc('Purchase Receipt', source_name)
@@ -601,11 +621,16 @@
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ target_doc.rejected_qty = 0
target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
- pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
+ qty = item_row.qty
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ qty = item_row.received_qty
+ pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
if returned_qty:
if returned_qty >= pending_qty:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 95096d7..d56822a 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -421,11 +421,18 @@
self.assertEqual(return_pr_2.items[0].qty, -3)
# Make PI against unreturned amount
+ buying_settings = frappe.get_single("Buying Settings")
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
+ buying_settings.save()
+
pi = make_purchase_invoice(pr.name)
pi.submit()
self.assertEqual(pi.items[0].qty, 3)
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1
+ buying_settings.save()
+
pr.load_from_db()
# PR should be completed on billing all unreturned amount
self.assertEqual(pr.items[0].billed_amt, 150)
@@ -767,8 +774,8 @@
pr1.items[0].purchase_receipt_item = pr.items[0].name
pr1.submit()
- pi = make_purchase_invoice(pr.name)
- self.assertEqual(pi.items[0].qty, 3)
+ pi1 = make_purchase_invoice(pr.name)
+ self.assertEqual(pi1.items[0].qty, 3)
pr1.cancel()
pr.reload()
@@ -1004,6 +1011,74 @@
self.assertEqual(pr.status, "To Bill")
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
+ def test_service_item_purchase_with_perpetual_inventory(self):
+ company = '_Test Company with perpetual inventory'
+ service_item = '_Test Non Stock Item'
+
+ before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items')
+ frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1)
+ srbnb_account = 'Stock Received But Not Billed - TCP1'
+ frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account)
+
+ pr = make_purchase_receipt(
+ company=company, item=service_item,
+ warehouse='Finished Goods - TCP1', do_not_save=1
+ )
+ item_row_with_diff_rate = frappe.copy_doc(pr.items[0])
+ item_row_with_diff_rate.rate = 100
+ pr.append('items', item_row_with_diff_rate)
+
+ pr.save()
+ pr.submit()
+
+ item_one_gl_entry = frappe.db.get_all("GL Entry", {
+ 'voucher_type': pr.doctype,
+ 'voucher_no': pr.name,
+ 'account': srbnb_account,
+ 'voucher_detail_no': pr.items[0].name
+ }, pluck="name")
+
+ item_two_gl_entry = frappe.db.get_all("GL Entry", {
+ 'voucher_type': pr.doctype,
+ 'voucher_no': pr.name,
+ 'account': srbnb_account,
+ 'voucher_detail_no': pr.items[1].name
+ }, pluck="name")
+
+ # check if the entries are not merged into one
+ # seperate entries should be made since voucher_detail_no is different
+ self.assertEqual(len(item_one_gl_entry), 1)
+ self.assertEqual(len(item_two_gl_entry), 1)
+
+ frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
+
+ def test_purchase_receipt_with_exchange_rate_difference(self):
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
+
+ pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
+
+ create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
+ properties={"account": '_Test Account Stock In Hand - TCP1'})
+
+ pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
+ company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
+ do_not_save = "True")
+
+ pr.items[0].purchase_invoice = pi.name
+ pr.items[0].purchase_invoice_item = pi.items[0].name
+
+ pr.insert()
+ pr.submit()
+
+ # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
+ gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
+ voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
+ self.assertEqual(pr.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 get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index 3acf3a9..a3d44af 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -57,7 +57,8 @@
"more_info",
"serial_no_details",
"company",
- "status"
+ "status",
+ "work_order"
],
"fields": [
{
@@ -422,12 +423,18 @@
"label": "Status",
"options": "\nActive\nInactive\nDelivered\nExpired",
"read_only": 1
+ },
+ {
+ "fieldname": "work_order",
+ "fieldtype": "Link",
+ "label": "Work Order",
+ "options": "Work Order"
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2020-07-20 20:50:16.660433",
+ "modified": "2021-01-08 14:31:15.375996",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index b236f6a..bad7b60 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -473,16 +473,13 @@
if s.strip()]
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
- serial_no_doc.update({
- "item_code": args.get("item_code"),
- "company": args.get("company"),
- "batch_no": args.get("batch_no"),
- "via_stock_ledger": args.get("via_stock_ledger") or True,
- "supplier": args.get("supplier"),
- "location": args.get("location"),
- "warehouse": (args.get("warehouse")
- if args.get("actual_qty", 0) > 0 else None)
- })
+ for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
+ if args.get(field):
+ serial_no_doc.set(field, args.get(field))
+
+ serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
+ serial_no_doc.warehouse = (args.get("warehouse")
+ if args.get("actual_qty", 0) > 0 else None)
if is_new:
serial_no_doc.serial_no = serial_no
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 66f8b63..8f27ef4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -498,6 +498,7 @@
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
outgoing_items_cost += flt(d.basic_amount)
+
return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
@@ -854,6 +855,7 @@
pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty")
+ pro_doc.update_batch_produced_qty(self)
if not pro_doc.operations:
pro_doc.set_actual_dates()
@@ -1076,18 +1078,54 @@
# in case of BOM
to_warehouse = item.get("default_warehouse")
+ args = {
+ "to_warehouse": to_warehouse,
+ "from_warehouse": "",
+ "qty": self.fg_completed_qty,
+ "item_name": item.item_name,
+ "description": item.description,
+ "stock_uom": item.stock_uom,
+ "expense_account": item.get("expense_account"),
+ "cost_center": item.get("buying_cost_center"),
+ "is_finished_item": 1
+ }
+
+ if self.work_order and self.pro_doc.has_batch_no:
+ self.set_batchwise_finished_goods(args, item)
+ else:
+ self.add_finisged_goods(args, item)
+
+ def set_batchwise_finished_goods(self, args, item):
+ qty = flt(self.fg_completed_qty)
+ filters = {
+ "reference_name": self.pro_doc.name,
+ "reference_doctype": self.pro_doc.doctype,
+ "qty_to_produce": (">", 0)
+ }
+
+ fields = ["qty_to_produce as qty", "produced_qty", "name"]
+
+ for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
+ batch_qty = flt(row.qty) - flt(row.produced_qty)
+ if not batch_qty:
+ continue
+
+ if qty <=0:
+ break
+
+ fg_qty = batch_qty
+ if batch_qty >= qty:
+ fg_qty = qty
+
+ qty -= batch_qty
+ args["qty"] = fg_qty
+ args["batch_no"] = row.name
+
+ self.add_finisged_goods(args, item)
+
+ def add_finisged_goods(self, args, item):
self.add_to_stock_entry_detail({
- item.name: {
- "to_warehouse": to_warehouse,
- "from_warehouse": "",
- "qty": self.fg_completed_qty,
- "item_name": item.item_name,
- "description": item.description,
- "stock_uom": item.stock_uom,
- "expense_account": item.get("expense_account"),
- "cost_center": item.get("buying_cost_center"),
- "is_finished_item": 1
- }
+ item.name: args
}, bom_no = self.bom_no)
def get_bom_raw_materials(self, qty):
@@ -1524,6 +1562,36 @@
material_requests.append(material_request)
frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
+ def set_serial_no_batch_for_finished_good(self):
+ args = {}
+ if self.pro_doc.serial_no:
+ self.get_serial_nos_for_fg(args)
+
+ for row in self.items:
+ if row.is_finished_item and row.item_code == self.pro_doc.production_item:
+ if args.get("serial_no"):
+ row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
+
+ def get_serial_nos_for_fg(self, args):
+ fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
+ "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
+
+ filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
+ ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
+
+ stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
+
+ if self.pro_doc.serial_no:
+ args["serial_no"] = self.get_available_serial_nos(stock_entries)
+
+ def get_available_serial_nos(self, stock_entries):
+ used_serial_nos = []
+ for row in stock_entries:
+ if row.serial_no:
+ used_serial_nos.extend(get_serial_nos(row.serial_no))
+
+ return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):
@@ -1635,6 +1703,10 @@
if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
+ if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
+ 'add_corrective_operation_cost_in_finished_good_valuation')):
+ operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
+
return operating_cost_per_unit
def get_used_alternative_items(purchase_order=None, work_order=None):
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 864ff48..a178283 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -18,6 +18,7 @@
"col_break2",
"is_finished_item",
"is_scrap_item",
+ "quality_inspection",
"subcontracted_item",
"section_break_8",
"description",
@@ -69,7 +70,6 @@
"putaway_rule",
"column_break_51",
"reference_purchase_receipt",
- "quality_inspection",
"job_card_item"
],
"fields": [
@@ -548,7 +548,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-11 13:47:50.158754",
+ "modified": "2021-04-22 20:08:23.799715",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 3badc7e..76a3f1a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -48,37 +48,54 @@
},
get_items: function(frm) {
- frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
+ let fields = [{
+ label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
"get_query": function() {
return {
"filters": {
"company": frm.doc.company,
}
- }
- }},
- function(data) {
- frappe.call({
- method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
- args: {
- warehouse: data.warehouse,
- posting_date: frm.doc.posting_date,
- posting_time: frm.doc.posting_time,
- company:frm.doc.company
- },
- callback: function(r) {
- var items = [];
- frm.clear_table("items");
- for(var i=0; i< r.message.length; i++) {
- var d = frm.add_child("items");
- $.extend(d, r.message[i]);
- if(!d.qty) d.qty = null;
- if(!d.valuation_rate) d.valuation_rate = null;
- }
- frm.refresh_field("items");
- }
- });
+ };
}
- , __("Get Items"), __("Update"));
+ }, {
+ label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
+ "get_query": function() {
+ return {
+ "filters": {
+ "disabled": 0,
+ }
+ };
+ }
+ }];
+
+ frappe.prompt(fields, function(data) {
+ frappe.call({
+ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
+ args: {
+ warehouse: data.warehouse,
+ posting_date: frm.doc.posting_date,
+ posting_time: frm.doc.posting_time,
+ company: frm.doc.company,
+ item_code: data.item_code
+ },
+ callback: function(r) {
+ frm.clear_table("items");
+ for (var i=0; i<r.message.length; i++) {
+ var d = frm.add_child("items");
+ $.extend(d, r.message[i]);
+
+ if (!d.qty) {
+ d.qty = 0;
+ }
+
+ if (!d.valuation_rate) {
+ d.valuation_rate = 0;
+ }
+ }
+ frm.refresh_field("items");
+ }
+ });
+ }, __("Get Items"), __("Update"));
},
posting_date: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 93ab40a..2956384 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -481,45 +481,99 @@
self._cancel()
@frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company):
+def get_items(warehouse, posting_date, posting_time, company, item_code=None):
+ items = [frappe._dict({
+ 'item_code': item_code,
+ 'warehouse': warehouse
+ })]
+
+ if not item_code:
+ items = get_items_for_stock_reco(warehouse, company)
+
+ res = []
+ itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
+
+ for d in items:
+ if d.item_code in itemwise_batch_data:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse,
+ posting_date, posting_time, with_valuation_rate=True)
+
+ for row in itemwise_batch_data.get(d.item_code):
+ args = get_item_data(row, row.qty, stock_bal[1])
+ res.append(args)
+ else:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
+ with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
+
+ args = get_item_data(d, stock_bal[0], stock_bal[1],
+ stock_bal[2] if cint(d.has_serial_no) else '')
+
+ res.append(args)
+
+ return res
+
+def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql("""
- select i.name, i.item_name, bin.warehouse, i.has_serial_no
+ select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabBin bin, tabItem i
- where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
- and i.has_variants = 0 and i.has_batch_no = 0
- and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
- """, (lft, rgt))
+ where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
+ and i.has_variants = 0 and exists(
+ select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
+ )
+ """, (lft, rgt), as_dict=1)
items += frappe.db.sql("""
- select i.name, i.item_name, id.default_warehouse, i.has_serial_no
+ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabItem i, `tabItem Default` id
where i.name = id.parent
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
- and i.is_stock_item = 1 and i.has_batch_no = 0
- and i.has_variants = 0 and i.disabled = 0 and id.company=%s
+ and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
group by i.name
- """, (lft, rgt, company))
+ """, (lft, rgt, company), as_dict=1)
- res = []
- for d in set(items):
- stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
- with_valuation_rate=True , with_serial_no=cint(d[3]))
+ return items
- if frappe.db.get_value("Item", d[0], "disabled") == 0:
- res.append({
- "item_code": d[0],
- "warehouse": d[2],
- "qty": stock_bal[0],
- "item_name": d[1],
- "valuation_rate": stock_bal[1],
- "current_qty": stock_bal[0],
- "current_valuation_rate": stock_bal[1],
- "current_serial_no": stock_bal[2] if cint(d[3]) else '',
- "serial_no": stock_bal[2] if cint(d[3]) else ''
- })
+def get_item_data(row, qty, valuation_rate, serial_no=None):
+ return {
+ 'item_code': row.item_code,
+ 'warehouse': row.warehouse,
+ 'qty': qty,
+ 'item_name': row.item_name,
+ 'valuation_rate': valuation_rate,
+ 'current_qty': qty,
+ 'current_valuation_rate': valuation_rate,
+ 'current_serial_no': serial_no,
+ 'serial_no': serial_no,
+ 'batch_no': row.get('batch_no')
+ }
- return res
+def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
+ from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+ itemwise_batch_data = {}
+
+ filters = frappe._dict({
+ 'warehouse': warehouse,
+ 'from_date': posting_date,
+ 'to_date': posting_date,
+ 'company': company
+ })
+
+ if item_code:
+ filters.item_code = item_code
+
+ columns, data = execute(filters)
+
+ for row in data:
+ itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
+ 'item_code': row[0],
+ 'warehouse': warehouse,
+ 'qty': row[8],
+ 'item_name': row[1],
+ 'batch_no': row[4]
+ }))
+
+ return itemwise_batch_data
@frappe.whitelist()
def get_stock_balance_for(item_code, warehouse,
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 746cbbf..ca174a3 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -436,20 +436,35 @@
return itemwise_barcode
@frappe.whitelist()
-def get_item_tax_info(company, tax_category, item_codes, item_rates=None):
+def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None):
out = {}
- if isinstance(item_codes, string_types):
+
+ if item_tax_templates is None:
+ item_tax_templates = {}
+
+ if item_rates is None:
+ item_rates = {}
+
+ if isinstance(item_codes, (str,)):
item_codes = json.loads(item_codes)
- if isinstance(item_rates, string_types):
+ if isinstance(item_rates, (str,)):
item_rates = json.loads(item_rates)
+ if isinstance(item_tax_templates, (str,)):
+ item_tax_templates = json.loads(item_tax_templates)
+
for item_code in item_codes:
- if not item_code or item_code[1] in out:
+ if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]):
continue
+
out[item_code[1]] = {}
item = frappe.get_cached_doc("Item", item_code[0])
- args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]}
+ args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])}
+
+ if item_tax_templates:
+ args.update({"item_tax_template": item_tax_templates.get(item_code[1])})
+
get_item_tax_template(args, item, out[item_code[1]])
out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True)
@@ -463,9 +478,7 @@
}
"""
item_tax_template = args.get("item_tax_template")
-
- if not item_tax_template:
- item_tax_template = _get_item_tax_template(args, item.taxes, out)
+ item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_group = item.item_group
@@ -508,7 +521,8 @@
return None
# do not change if already a valid template
- if args.get('item_tax_template') in taxes:
+ if args.get('item_tax_template') in {t.item_tax_template for t in taxes}:
+ out["item_tax_template"] = args.get('item_tax_template')
return args.get('item_tax_template')
for tax in taxes:
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
new file mode 100644
index 0000000..bf11277
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Balance Qty After Transaction"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldtype: "Link",
+ fieldname: "company",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __('Item Code'),
+ fieldtype: 'Link',
+ fieldname: 'item_code',
+ options: 'Item'
+ },
+ {
+ label: __('Warehouse'),
+ fieldtype: 'Link',
+ fieldname: 'warehouse'
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
new file mode 100644
index 0000000..a5815bc
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-12 16:47:58.717853",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-12 16:48:28.347575",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Balance Qty After Transaction",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Balance Qty After Transaction",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Stock Manager"
+ },
+ {
+ "role": "Purchase User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
new file mode 100644
index 0000000..cf174c9
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from six import iteritems
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_data(filters):
+ data = get_stock_ledger_entries(filters)
+ itewise_balance_qty = {}
+
+ for row in data:
+ key = (row.item_code, row.warehouse)
+ itewise_balance_qty.setdefault(key, []).append(row)
+
+ res = validate_data(itewise_balance_qty)
+ return res
+
+def validate_data(itewise_balance_qty):
+ res = []
+ for key, data in iteritems(itewise_balance_qty):
+ row = get_incorrect_data(data)
+ if row:
+ res.append(row)
+ res.append({})
+
+ return res
+
+def get_incorrect_data(data):
+ balance_qty = 0.0
+ for row in data:
+ balance_qty += row.actual_qty
+ if row.voucher_type == "Stock Reconciliation" and not row.batch_no:
+ balance_qty = flt(row.qty_after_transaction)
+
+ row.expected_balance_qty = balance_qty
+ if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5:
+ row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction))
+ return row
+
+def get_stock_ledger_entries(report_filters):
+ filters = {}
+ fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
+ 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
+
+ for field in ['warehouse', 'item_code', 'company']:
+ if report_filters.get(field):
+ filters[field] = report_filters.get(field)
+
+ return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+ order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+ return [{
+ 'label': _('Id'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'name',
+ 'options': 'Stock Ledger Entry',
+ 'width': 120
+ }, {
+ 'label': _('Posting Date'),
+ 'fieldtype': 'Date',
+ 'fieldname': 'posting_date',
+ 'width': 110
+ }, {
+ 'label': _('Voucher Type'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'voucher_type',
+ 'options': 'DocType',
+ 'width': 120
+ }, {
+ 'label': _('Voucher No'),
+ 'fieldtype': 'Dynamic Link',
+ 'fieldname': 'voucher_no',
+ 'options': 'voucher_type',
+ 'width': 120
+ }, {
+ 'label': _('Item Code'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'item_code',
+ 'options': 'Item',
+ 'width': 120
+ }, {
+ 'label': _('Warehouse'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'warehouse',
+ 'options': 'Warehouse',
+ 'width': 120
+ }, {
+ 'label': _('Expected Balance Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'expected_balance_qty',
+ 'width': 170
+ }, {
+ 'label': _('Actual Balance Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'qty_after_transaction',
+ 'width': 150
+ }, {
+ 'label': _('Difference'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'differnce',
+ 'width': 110
+ }]
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
new file mode 100644
index 0000000..c62d480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Serial No Valuation"] = {
+ "filters": [
+ {
+ label: __('Item Code'),
+ fieldtype: 'Link',
+ fieldname: 'item_code',
+ options: 'Item',
+ get_query: function() {
+ return {
+ filters: {
+ 'has_serial_no': 1
+ }
+ }
+ }
+ },
+ {
+ label: __('From Date'),
+ fieldtype: 'Date',
+ fieldname: 'from_date',
+ reqd: 1,
+ default: frappe.defaults.get_user_default("year_start_date")
+ },
+ {
+ label: __('To Date'),
+ fieldtype: 'Date',
+ fieldname: 'to_date',
+ reqd: 1,
+ default: frappe.defaults.get_user_default("year_end_date")
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
new file mode 100644
index 0000000..cc384a5
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-13 13:07:00.767845",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-05-13 13:07:00.767845",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Serial No Valuation",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Serial No Valuation",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Stock Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
new file mode 100644
index 0000000..e54cf4c
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import copy
+from frappe import _
+from six import iteritems
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_data(filters):
+ data = get_stock_ledger_entries(filters)
+ serial_nos_data = prepare_serial_nos(data)
+ data = get_incorrect_serial_nos(serial_nos_data)
+
+ return data
+
+def prepare_serial_nos(data):
+ serial_no_wise_data = {}
+ for row in data:
+ if not row.serial_nos:
+ continue
+
+ for serial_no in get_serial_nos(row.serial_nos):
+ sle = copy.deepcopy(row)
+ sle.serial_no = serial_no
+ sle.qty = 1 if sle.actual_qty > 0 else -1
+ sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1
+ serial_no_wise_data.setdefault(serial_no, []).append(sle)
+
+ return serial_no_wise_data
+
+def get_incorrect_serial_nos(serial_nos_data):
+ result = []
+
+ total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))})
+
+ for serial_no, data in iteritems(serial_nos_data):
+ total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))})
+
+ if check_incorrect_serial_data(data, total_dict):
+ result.extend(data)
+
+ total_value.qty += total_dict.qty
+ total_value.valuation_rate += total_dict.valuation_rate
+
+ result.append(total_dict)
+ result.append({})
+
+ result.append(total_value)
+
+ return result
+
+def check_incorrect_serial_data(data, total_dict):
+ incorrect_data = False
+ for row in data:
+ total_dict.qty += row.qty
+ total_dict.valuation_rate += row.valuation_rate
+
+ if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0):
+ incorrect_data = True
+
+ return incorrect_data
+
+def get_stock_ledger_entries(report_filters):
+ fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
+ 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
+
+ filters = {'serial_no': ("is", "set")}
+
+ if report_filters.get('item_code'):
+ filters['item_code'] = report_filters.get('item_code')
+
+ if report_filters.get('from_date') and report_filters.get('to_date'):
+ filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')])
+
+ return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+ order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+ return [{
+ 'label': _('Company'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'company',
+ 'options': 'Company',
+ 'width': 120
+ }, {
+ 'label': _('Id'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'name',
+ 'options': 'Stock Ledger Entry',
+ 'width': 120
+ }, {
+ 'label': _('Posting Date'),
+ 'fieldtype': 'Date',
+ 'fieldname': 'posting_date',
+ 'width': 90
+ }, {
+ 'label': _('Posting Time'),
+ 'fieldtype': 'Time',
+ 'fieldname': 'posting_time',
+ 'width': 90
+ }, {
+ 'label': _('Voucher Type'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'voucher_type',
+ 'options': 'DocType',
+ 'width': 100
+ }, {
+ 'label': _('Voucher No'),
+ 'fieldtype': 'Dynamic Link',
+ 'fieldname': 'voucher_no',
+ 'options': 'voucher_type',
+ 'width': 110
+ }, {
+ 'label': _('Item Code'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'item_code',
+ 'options': 'Item',
+ 'width': 120
+ }, {
+ 'label': _('Warehouse'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'warehouse',
+ 'options': 'Warehouse',
+ 'width': 120
+ }, {
+ 'label': _('Serial No'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'serial_no',
+ 'options': 'Serial No',
+ 'width': 100
+ }, {
+ 'label': _('Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'qty',
+ 'width': 80
+ }, {
+ 'label': _('Valuation Rate (In / Out)'),
+ 'fieldtype': 'Currency',
+ 'fieldname': 'valuation_rate',
+ 'width': 110
+ }]
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
new file mode 100644
index 0000000..ff42480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Stock Value Report"] = {
+ "filters": [
+ {
+ "label": __("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "label": __("Account"),
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "options": "Account",
+ get_query: function() {
+ var company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: {
+ "account_type": "Stock",
+ "company": company
+ }
+ }
+ }
+ },
+ {
+ "label": __("From Date"),
+ "fieldname": "from_date",
+ "fieldtype": "Date"
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
new file mode 100644
index 0000000..a7e9f20
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-22 15:35:05.148177",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-22 15:35:05.148177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Stock Value Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Stock Value Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
new file mode 100644
index 0000000..a724387
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from six import iteritems
+from frappe.utils import add_days, today, getdate
+from erpnext.stock.utils import get_stock_value_on
+from erpnext.accounts.utils import get_stock_and_account_balance
+
+def execute(filters=None):
+ if not erpnext.is_perpetual_inventory_enabled(filters.company):
+ frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
+ .format(filters.company))
+
+ data = get_data(filters)
+ columns = get_columns(filters)
+
+ return columns, data
+
+def get_unsync_date(filters):
+ date = filters.from_date
+ if not date:
+ date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+ date = date[0][0]
+
+ if not date:
+ return
+
+ while getdate(date) < getdate(today()):
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
+ company=filters.company, account = filters.account)
+
+ if abs(account_bal - stock_bal) > 0.1:
+ return date
+
+ date = add_days(date, 1)
+
+def get_data(report_filters):
+ from_date = get_unsync_date(report_filters)
+
+ if not from_date:
+ return []
+
+ result = []
+
+ voucher_wise_dict = {}
+ data = frappe.db.sql('''
+ SELECT
+ name, posting_date, posting_time, voucher_type, voucher_no,
+ stock_value_difference, stock_value, warehouse, item_code
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ posting_date
+ = %s and company = %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', (from_date, report_filters.company), as_dict=1)
+
+ for d in data:
+ voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
+
+ closing_date = add_days(from_date, -1)
+ for key, stock_data in iteritems(voucher_wise_dict):
+ prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
+ for data in stock_data:
+ expected_stock_value = prev_stock_value + data.stock_value_difference
+ if abs(data.stock_value - expected_stock_value) > 0.1:
+ data.difference_value = abs(data.stock_value - expected_stock_value)
+ data.expected_stock_value = expected_stock_value
+ result.append(data)
+
+ return result
+
+def get_columns(filters):
+ return [
+ {
+ "label": _("Stock Ledger ID"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Stock Ledger Entry",
+ "width": "80"
+ },
+ {
+ "label": _("Posting Date"),
+ "fieldname": "posting_date",
+ "fieldtype": "Date"
+ },
+ {
+ "label": _("Posting Time"),
+ "fieldname": "posting_time",
+ "fieldtype": "Time"
+ },
+ {
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Voucher No"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": "110"
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": "110"
+ },
+ {
+ "label": _("Expected Stock Value"),
+ "fieldname": "expected_stock_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ },
+ {
+ "label": _("Stock Value"),
+ "fieldname": "stock_value",
+ "fieldtype": "Currency",
+ "width": "120"
+ },
+ {
+ "label": _("Difference Value"),
+ "fieldname": "difference_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 3221dc4..529ce8e 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -15,6 +15,7 @@
"hide_custom": 0,
"icon": "stock",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Stock",
"links": [
@@ -653,9 +654,44 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Data Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Serial No Qty and Valuation",
+ "link_to": "Incorrect Serial No Valuation",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Balance Qty After Transaction",
+ "link_to": "Incorrect Balance Qty After Transaction",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock and Account Value Comparison",
+ "link_to": "Stock and Account Value Comparison",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2020-12-01 13:38:36.282890",
+ "modified": "2021-05-13 13:10:24.914983",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index bc29821..14712f8 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -166,7 +166,7 @@
"options": "Service Level Agreement"
},
{
- "depends_on": "eval: doc.status != 'Replied';",
+ "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "response_by",
"fieldtype": "Datetime",
"label": "Response By",
@@ -180,7 +180,7 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.status != 'Replied';",
+ "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "resolution_by",
"fieldtype": "Datetime",
"label": "Resolution By",
@@ -410,7 +410,7 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2021-05-26 10:49:07.574769",
+ "modified": "2021-06-10 03:22:27.098898",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index dd6d647..e092b07 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -26,6 +26,9 @@
self.set_lead_contact(self.raised_by)
+ if not self.service_level_agreement:
+ self.reset_sla_fields()
+
def on_update(self):
# Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal:
@@ -51,6 +54,106 @@
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company")
+ def reset_sla_fields(self):
+ self.agreement_status = ""
+ self.response_by = ""
+ self.resolution_by = ""
+ self.response_by_variance = 0
+ self.resolution_by_variance = 0
+
+ def update_status(self):
+ status = frappe.db.get_value("Issue", self.name, "status")
+ if self.status != "Open" and status == "Open" and not self.first_responded_on:
+ self.first_responded_on = frappe.flags.current_time or now_datetime()
+
+ if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
+ self.resolution_date = frappe.flags.current_time or now_datetime()
+ if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
+ set_service_level_agreement_variance(issue=self.name)
+ self.update_agreement_status()
+ set_resolution_time(issue=self)
+ set_user_resolution_time(issue=self)
+
+ if self.status == "Open" and status != "Open":
+ # if no date, it should be set as None and not a blank string "", as per mysql strict config
+ self.resolution_date = None
+ self.reset_issue_metrics()
+ # enable SLA and variance on Reopen
+ self.agreement_status = "Ongoing"
+ set_service_level_agreement_variance(issue=self.name)
+
+ self.handle_hold_time(status)
+
+ def handle_hold_time(self, status):
+ if self.service_level_agreement:
+ # set response and resolution variance as None as the issue is on Hold
+ pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
+ filters={"parent": self.service_level_agreement})
+ hold_statuses = [entry.status for entry in pause_sla_on]
+ update_values = {}
+
+ if hold_statuses:
+ if self.status in hold_statuses and status not in hold_statuses:
+ update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
+ if not self.first_responded_on:
+ update_values['response_by'] = None
+ update_values['response_by_variance'] = 0
+ update_values['resolution_by'] = None
+ update_values['resolution_by_variance'] = 0
+
+ # calculate hold time when status is changed from any hold status to any non-hold status
+ if self.status not in hold_statuses and status in hold_statuses:
+ hold_time = self.total_hold_time if self.total_hold_time else 0
+ now_time = frappe.flags.current_time or now_datetime()
+ last_hold_time = 0
+ if self.on_hold_since:
+ # last_hold_time will be added to the sla variables
+ last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
+ update_values['total_hold_time'] = hold_time + last_hold_time
+
+ # re-calculate SLA variables after issue changes from any hold status to any non-hold status
+ # add hold time to SLA variables
+ start_date_time = get_datetime(self.service_level_agreement_creation)
+ priority = get_priority(self)
+ now_time = frappe.flags.current_time or now_datetime()
+
+ if not self.first_responded_on:
+ response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+ response_by = add_to_date(response_by, seconds=round(last_hold_time))
+ response_by_variance = round(time_diff_in_seconds(response_by, now_time))
+ update_values['response_by'] = response_by
+ update_values['response_by_variance'] = response_by_variance + last_hold_time
+
+ resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+ resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
+ resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
+ update_values['resolution_by'] = resolution_by
+ update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
+ update_values['on_hold_since'] = None
+
+ self.db_set(update_values)
+
+ def update_agreement_status(self):
+ if self.service_level_agreement and self.agreement_status == "Ongoing":
+ if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \
+ cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0:
+
+ self.agreement_status = "Failed"
+ else:
+ self.agreement_status = "Fulfilled"
+
+ def update_agreement_status_on_custom_status(self):
+ """
+ Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
+ """
+ if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
+ self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
+
+ if not self.resolution_date: # resolution_date set when issue has been closed
+ self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
+
+ self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
+
def create_communication(self):
communication = frappe.new_doc("Communication")
communication.update({
@@ -215,4 +318,4 @@
def get_holidays(holiday_list_name):
holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
- return holidays
\ No newline at end of file
+ return holidays
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
index 576e0b7..18691fe 100644
--- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
@@ -22,10 +22,10 @@
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 @@
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);
}
}
}
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index 84a9430..4482bc1 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -99,6 +99,7 @@
fieldname: 'country',
fieldtype: 'Link',
options: 'Country',
+ only_select: true,
reqd: 1
},
{
diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html
index 4c8c40d..a256fbd 100644
--- a/erpnext/templates/includes/projects/project_row.html
+++ b/erpnext/templates/includes/projects/project_row.html
@@ -1,28 +1,54 @@
-{% if doc.status=="Open" %}
-<div class="web-list-item">
- <a class="no-decoration" href="/projects?project={{ doc.name | urlencode }}">
- <div class="row">
- <div class="col-xs-6">
-
- {{ doc.name }}
- </div>
- <div class="col-xs-3">
- {% if doc.percent_complete %}
- <div class="progress" style="margin-bottom: 0!important; margin-top: 10px!important; height:5px;">
- <div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success"}}" role="progressbar"
- aria-valuenow="{{ doc.percent_complete|round|int }}"
- aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
- </div>
- </div>
- {% else %}
- <span class="indicator {{ "red" if doc.status=="Open" else "gray" }}">
- {{ doc.status }}</span>
- {% endif %}
- </div>
- <div class="col-xs-3 text-right small text-muted">
- {{ frappe.utils.pretty_date(doc.modified) }}
- </div>
- </div>
- </a>
-</div>
+{% if doc.status == "Open" %}
+ <div class="web-list-item transaction-list-item">
+ <div class="row">
+ <div class="col-xs-2">
+ <a class="transaction-item-link" href="/projects?project={{ doc.name | urlencode }}">Link</a>
+ {{ doc.name }}
+ </div>
+ <div class="col-xs-2">
+ {{ doc.project_name }}
+ </div>
+ <div class="col-xs-3 text-center">
+ {% if doc.percent_complete %}
+ {% set pill_class = "green" if doc.percent_complete | round == 100 else
+ "orange" %}
+ <div class="ellipsis">
+ <span class="indicator-pill {{ pill_class }} filterable ellipsis">
+ <span>{{ frappe.utils.cint(doc.percent_complete) }}
+ %</span>
+ </span>
+ </div>
+ {% else %}
+ <span class="indicator-pill {{ " red" if doc.status=="Open" else "darkgrey" }}">
+ {{ doc.status }}</span>
+ {% endif %}
+ </div>
+ {% if doc["_assign"] %}
+ {% set assigned_users = json.loads(doc["_assign"])%}
+ <div class="col-xs-2">
+ {% for user in assigned_users %}
+ {% set user_details = frappe
+ .db
+ .get_value("User", user, [
+ "full_name", "user_image"
+ ], as_dict = True) %}
+ {% if user_details.user_image %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <img src="{{ user_details.user_image }}">
+ </span>
+ {% else %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <div class='standard-image' style="background-color: #F5F4F4; color: #000;">
+ {{ frappe.utils.get_abbr(user_details.full_name) }}
+ </div>
+ </span>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% endif %}
+ <div class="col-xs-3 text-right small text-muted">
+ {{ frappe.utils.pretty_date(doc.modified) }}
+ </div>
+ </div>
+ </div>
{% endif %}
diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html
index 50b9f4b..2b07a5f 100644
--- a/erpnext/templates/includes/projects/project_tasks.html
+++ b/erpnext/templates/includes/projects/project_tasks.html
@@ -1,32 +1,5 @@
{% for task in doc.tasks %}
- <div class='task'>
- <a class="no-decoration task-link {{ task.css_seen }}" href="/tasks?name={{ task.name }}">
- <div class='row project-item'>
- <div class='col-xs-9'>
- <span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "gray" }}" title="{{ task.status }}" > {{ task.subject }}</span>
- <div class="small text-muted item-timestamp"
- title="{{ frappe.utils.pretty_date(task.modified) }}">
- {{ _("modified") }} {{ frappe.utils.pretty_date(task.modified) }}
- </div>
- </div>
- <div class='col-xs-1'>{% if task.todo %}
- {% if task.todo.user_image %}
- <span class="avatar avatar-small" title="{{ task.todo.owner }}">
- <img src="{{ task.todo.user_image }}">
- </span>
- {% else %}
- <span class="avatar avatar-small standard-image" title="Assigned to {{ task.todo.owner }}">
-
- </span>
- {% endif %}
- {% endif %} </div>
- <div class='col-xs-2'>
- <span class="pull-right list-comment-count small {{ "text-extra-muted" if task.comment_count==0 else "text-muted" }}">
- <i class="octicon octicon-comment-discussion"></i>
- {{ task.comment_count }}
- </span>
- </div>
- </div>
- </a>
- </div>
+ <div class="web-list-item transaction-list-item">
+ {{ task_row(task, 0) }}
+ </div>
{% endfor %}
diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html
index 05a07c1..fa5b2f9 100644
--- a/erpnext/templates/includes/projects/project_timesheets.html
+++ b/erpnext/templates/includes/projects/project_timesheets.html
@@ -1,23 +1,33 @@
{% for timesheet in doc.timesheets %}
-<div class='timesheet'>
- <a class="no-decoration timesheet-link {{ timesheet.css_seen }}" href="/timesheet/{{ timesheet.info.name}}">
- <div class='row project-item'>
- <div class='col-xs-10'>
- <span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "gray" }}" title="{{ timesheet.info.status }}" > {{ timesheet.info.name }} </span>
- <div class="small text-muted item-timestamp">
- {{ _("From") }} {{ frappe.format_date(timesheet.from_time) }} {{ _("to") }} {{ frappe.format_date(timesheet.to_time) }}
- </div>
- </div>
- <div class='col-xs-1' style="margin-right:-30px;">
- <span class="avatar avatar-small" title="{{ timesheet.info.modified_by }}"> <img src="{{ timesheet.info.user_image }}" style="display:flex;"></span>
- </div>
- <div class='col-xs-1'>
- <span class="pull-right list-comment-count small {{ "text-extra-muted" if timesheet.comment_count==0 else "text-muted" }}">
- <i class="octicon octicon-comment-discussion"></i>
- {{ timesheet.info.comment_count }}
- </span>
- </div>
- </div>
- </a>
-</div>
-{% endfor %}
\ No newline at end of file
+ <div class="web-list-item transaction-list-item">
+ <div class="row">
+ <div class="col-xs-2">{{ timesheet.name }}</div>
+ <a class="transaction-item-link" href="/timesheet/{{ timesheet.name}}">Link</a>
+ <div class="col-xs-2">{{ timesheet.status }}</div>
+ <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.from_time, "medium") }}</div>
+ <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.to_time, "medium") }}</div>
+ <div class="col-xs-2">
+ {% set user_details = frappe
+ .db
+ .get_value("User", timesheet.modified_by, [
+ "full_name", "user_image"
+ ], as_dict = True)
+ %}
+ {% if user_details.user_image %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <img src="{{ user_details.user_image }}">
+ </span>
+ {% else %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
+ {{ frappe.utils.get_abbr(user_details.full_name) }}
+ </div>
+ </span>
+ {% endif %}
+ </div>
+ <div class="col-xs-2 text-right">
+ {{ frappe.utils.pretty_date(timesheet.modified) }}
+ </div>
+ </div>
+ </div>
+{% endfor %}
diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html
index 7e294e0..76eaf75 100644
--- a/erpnext/templates/pages/projects.html
+++ b/erpnext/templates/pages/projects.html
@@ -1,90 +1,173 @@
{% extends "templates/web.html" %}
-{% block title %}{{ doc.project_name }}{% endblock %}
+{% block title %}
+ {{ doc.project_name }}
+{% endblock %}
+
+{% block head_include %}
+ <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
+{% endblock %}
{% block header %}
- <h1>{{ doc.project_name }}</h1>
+ <h1>{{ doc.project_name }}</h1>
{% endblock %}
{% block style %}
- <style>
- {% include "templates/includes/projects.css" %}
- </style>
+ <style>
+ {
+ % include "templates/includes/projects.css"%
+ }
+ </style>
{% endblock %}
-
{% block page_content %}
-{% if doc.percent_complete %}
-<div class="progress progress-hg">
- <div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success" }} active" role="progressbar" aria-valuenow="{{ doc.percent_complete|round|int }}"
- aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
- </div>
-</div>
-{% endif %}
-<div class="clearfix">
- <h4 style="float: left;">{{ _("Tasks") }}</h4>
- <a class="btn btn-secondary btn-light btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
-</div>
+ {{ progress_bar(doc.percent_complete) }}
-<p>
-<!-- <a class='small underline task-status-switch' data-status='Open'>{{ _("Show closed") }}</a> -->
-</p>
+ <div class="d-flex mt-5 mb-5 justify-content-between">
+ <h4>Status:</h4>
+ <h4>Progress:
+ <span>{{ doc.percent_complete }}
+ %</span>
+ </h4>
+ <h4>Hours Spent:
+ <span>{{ doc.actual_time }}</span>
+ </h4>
+ </div>
-{% if doc.tasks %}
- <div class='project-task-section'>
- <div class='project-task'>
- {% include "erpnext/templates/includes/projects/project_tasks.html" %}
- </div>
- <p><a id= 'more-task' style='display: none;' class='more-tasks small underline'>{{ _("More") }}</a><p>
- </div>
-{% else %}
- <p class="text-muted">{{ _("No tasks") }}</p>
-{% endif %}
+ {{ progress_bar(doc.percent_complete) }}
+ {% if doc.tasks %}
+ <div class="website-list">
+ <div class="result">
+ <div class="web-list-item transaction-list-item">
+ <div class="row">
+ <h3 class="col-xs-4">Tasks</h3>
+ <h3 class="col-xs-2">Status</h3>
+ <h3 class="col-xs-2">End Date</h3>
+ <h3 class="col-xs-2">Assigned To</h3>
+ <div class="col-xs-2 text-right">
+ <a class="btn btn-secondary btn-light btn-sm" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
+ </div>
+ </div>
+ </div>
+ {% include "erpnext/templates/includes/projects/project_tasks.html" %}
+ </div>
+ </div>
+ {% else %}
+ <p class="font-weight-bold">{{ _("No Tasks") }}</p>
+ {% endif %}
-<div class='padding'></div>
+ {% if doc.timesheets %}
+ <div class="website-list">
+ <div class="result">
+ <div class="web-list-item transaction-list-item">
+ <div class="row">
+ <h3 class="col-xs-2">Timesheets</h3>
+ <h3 class="col-xs-2">Status</h3>
+ <h3 class="col-xs-2">From</h3>
+ <h3 class="col-xs-2">To</h3>
+ <h3 class="col-xs-2">Modified By</h3>
+ <h3 class="col-xs-2 text-right">Modified On</h3>
+ </div>
+ </div>
+ {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
+ </div>
+ </div>
+ {% else %}
+ <p class="font-weight-bold mt-5">{{ _("No Timesheets") }}</p>
+ {% endif %}
-<h4>{{ _("Timesheets") }}</h4>
+ {% if doc.attachments %}
+ <div class='padding'></div>
-{% if doc.timesheets %}
- <div class='project-timelogs'>
- {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
- </div>
- {% if doc.timesheets|length > 9 %}
- <p><a class='more-timelogs small underline'>{{ _("More") }}</a><p>
- {% endif %}
-{% else %}
- <p class="text-muted">{{ _("No time sheets") }}</p>
-{% endif %}
-
-{% if doc.attachments %}
-<div class='padding'></div>
-
-<h4>{{ _("Attachments") }}</h4>
- <div class="project-attachments">
- {% for attachment in doc.attachments %}
- <div class="attachment">
- <a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
- <div class="row">
- <div class="col-xs-9">
- <span class="indicator red file-name"> {{ attachment.file_name }}</span>
- </div>
- <div class="col-xs-3">
- <span class="pull-right file-size">{{ attachment.file_size }}</span>
- </div>
- </div>
- </a>
- </div>
- {% endfor %}
- </div>
-{% endif %}
+ <h4>{{ _("Attachments") }}</h4>
+ <div class="project-attachments">
+ {% for attachment in doc.attachments %}
+ <div class="attachment">
+ <a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
+ <div class="row">
+ <div class="col-xs-9">
+ <span class="indicator red file-name">
+ {{ attachment.file_name }}</span>
+ </div>
+ <div class="col-xs-3">
+ <span class="pull-right file-size">{{ attachment.file_size }}</span>
+ </div>
+ </div>
+ </a>
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
</div>
<script>
- {% include "frappe/public/js/frappe/provide.js" %}
- {% include "frappe/public/js/frappe/form/formatters.js" %}
+ { % include "frappe/public/js/frappe/provide.js" %
+ } { % include "frappe/public/js/frappe/form/formatters.js" %
+ }
</script>
{% endblock %}
+
+{% macro progress_bar(percent_complete) %}
+{% if percent_complete %}
+ <div class="progress progress-hg" style="height: 5px;">
+ <div class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active" role="progressbar" aria-valuenow="{{ percent_complete|round|int }}" aria-valuemin="0" aria-valuemax="100" style="width:{{ percent_complete|round|int }}%;"></div>
+ </div>
+{% else %}
+ <hr>
+{% endif %}
+{% endmacro %}
+
+{% macro task_row(task, indent) %}
+<div class="row mt-5 {% if task.children %} font-weight-bold {% endif %}">
+ <div class="col-xs-4">
+ <a class="nav-link " style="color: inherit; {% if task.parent_task %} margin-left: {{ indent }}px {% endif %}" href="/tasks?name={{ task.name | urlencode }}">
+ {% if task.parent_task %}
+ <span class="">
+ <i class="fa fa-level-up fa-rotate-90"></i>
+ </span>
+ {% endif %}
+ {{ task.subject }}</a>
+ </div>
+ <div class="col-xs-2">{{ task.status }}</div>
+ <div class="col-xs-2">
+ {% if task.exp_end_date %}
+ {{ task.exp_end_date }}
+ {% else %}
+ --
+ {% endif %}
+ </div>
+ <div class="col-xs-2">
+ {% if task["_assign"] %}
+ {% set assigned_users = json.loads(task["_assign"])%}
+ {% for user in assigned_users %}
+ {% set user_details = frappe.db.get_value("User", user,
+ ["full_name", "user_image"],
+ as_dict = True)%}
+ {% if user_details.user_image %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <img src="{{ user_details.user_image }}">
+ </span>
+ {% else %}
+ <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+ <div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
+ {{ frappe.utils.get_abbr(user_details.full_name) }}
+ </div>
+ </span>
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ </div>
+ <div class="col-xs-2 text-right">
+ {{ frappe.utils.pretty_date(task.modified) }}
+ </div>
+</div>
+{% if task.children %}
+ {% for child in task.children %}
+ {{ task_row(child, indent + 30) }}
+ {% endfor %}
+{% endif %}
+{% endmacro %}
diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py
index d23fed9..7ff4954 100644
--- a/erpnext/templates/pages/projects.py
+++ b/erpnext/templates/pages/projects.py
@@ -32,29 +32,17 @@
filters = {"project": project}
if search:
filters["subject"] = ("like", "%{0}%".format(search))
- # if item_status:
-# filters["status"] = item_status
tasks = frappe.get_all("Task", filters=filters,
- fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"],
+ fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"],
limit_start=start, limit_page_length=10)
-
+ task_nest = []
for task in tasks:
- task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'},
- fields=["assigned_by", "owner", "modified", "modified_by"])
-
- if task.todo:
- task.todo=task.todo[0]
- task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image')
-
-
- task.comment_count = len(json.loads(task._comments or "[]"))
-
- task.css_seen = ''
- if task._seen:
- if frappe.session.user in json.loads(task._seen):
- task.css_seen = 'seen'
-
- return tasks
+ if task.is_group:
+ child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks))
+ if len(child_tasks):
+ task.children = child_tasks
+ task_nest.append(task)
+ return list(filter(lambda x: not x.parent_task, tasks))
@frappe.whitelist()
def get_task_html(project, start=0, item_status=None):
@@ -74,19 +62,11 @@
fields=['project','activity_type','from_time','to_time','parent'],
limit_start=start, limit_page_length=10)
for timesheet in timesheets:
- timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
- fields=['name','_comments','_seen','status','modified','modified_by'],
+ info = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
+ fields=['name','status','modified','modified_by'],
limit_start=start, limit_page_length=10)
-
- for timesheet.info in timesheet.infos:
- timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image')
-
- timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]"))
-
- timesheet.info.css_seen = ''
- if timesheet.info._seen:
- if frappe.session.user in json.loads(timesheet.info._seen):
- timesheet.info.css_seen = 'seen'
+ if len(info):
+ timesheet.update(info[0])
return timesheets
@frappe.whitelist()