Merge pull request #26183 from deepeshgarg007/item_tax_fetch_fix_develop
fix: User is not able to change item tax template
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 60c614f..39d9a27 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.5.2'
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/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 1a6dbed..7c1a171 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')
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.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/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ff433b9..2f5d36c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -966,7 +966,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 +1002,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/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/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/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/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index ae02c51..ae009ba 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..49dd701 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,53 @@
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": nowdate(),
+ "to_date": add_days(nowdate(), 10),
+ "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/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/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d1f6385..3f109d9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -81,7 +81,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') }]
@@ -213,7 +213,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 +242,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 +403,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
@@ -975,7 +990,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..1f443fb 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -123,7 +123,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)
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/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 3e5a72d..8088d93 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -704,6 +704,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/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/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 7a70679..e71d81f 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
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/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/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/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/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 dd94c26..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 = []
@@ -66,7 +76,6 @@
)
items_dict = {item.name: item for item in items}
- # TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys()))
@@ -78,14 +87,22 @@
filters=self.filters,
or_filters=self.or_filters,
start=start,
- limit=self.page_length,
- order_by="weightage desc"
+ 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
@@ -99,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.py b/erpnext/stock/doctype/batch/batch.py
index 508e17c..30bdbe8 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,4 @@
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))
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.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/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 99abf3a..2586a0f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1011,6 +1011,47 @@
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 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/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
},
{