diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 09feb7d97bc..27541670a89 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -31,13 +31,19 @@ apply_on_table = {
}
def get_pricing_rules(args, doc=None):
- pricing_rules = []
- values = {}
+ pricing_rules_all = []
+ values = {}
for apply_on in ['Item Code', 'Item Group', 'Brand']:
- pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
- if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
- break
+ pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values))
+
+ # removing duplicate pricing rule
+ pricing_rules_name = []
+ pricing_rules = []
+ for p in pricing_rules_all:
+ if p['name'] not in pricing_rules_name:
+ pricing_rules_name.append(p['name'])
+ pricing_rules.append(p)
rules = []
@@ -323,9 +329,10 @@ def apply_internal_priority(pricing_rules, field_set, args):
filtered_rules = []
for field in field_set:
if args.get(field):
- # filter function always returns a filter object even if empty
- # list conversion is necessary to check for an empty result
- filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules))
+ for rule in pricing_rules:
+ if rule.get(field) == args.get(field):
+ filtered_rules = [rule]
+ break
if filtered_rules: break
return filtered_rules or pricing_rules
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c9fd889b63f..155bfd4416a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -636,7 +636,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate)
}, item=item))
else:
- cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company)
+ cwip_account = get_asset_account("capital_work_in_progress_account",
+ asset_category=item.asset_category,company=self.company)
cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 4019815e19a..329433ebc10 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -939,7 +939,8 @@ def make_purchase_invoice(**args):
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
- "rejected_serial_no": args.rejected_serial_no or ""
+ "rejected_serial_no": args.rejected_serial_no or "",
+ "asset_location": args.location or ""
})
if args.get_taxes_and_charges:
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 719b8de92be..f03d1e43c42 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -470,29 +470,37 @@ class Asset(AccountsController):
def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document()
- asset_bought_with_invoice = purchase_document == self.purchase_invoice
- fixed_asset_account, cwip_account = self.get_asset_accounts()
- cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
- # check if expense already has been booked in case of cwip was enabled after purchasing asset
- expense_booked = False
- cwip_booked = False
-
- if asset_bought_with_invoice:
- expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
- (purchase_document, fixed_asset_account), as_dict=1)
- else:
- cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
- (purchase_document, cwip_account), as_dict=1)
-
- if cwip_enabled and (expense_booked or not cwip_booked):
- # if expense has already booked from invoice or cwip is booked from receipt
+ if not purchase_document:
return False
- elif not cwip_enabled and (not expense_booked or cwip_booked):
- # if cwip is disabled but expense hasn't been booked yet
- return True
- elif cwip_enabled:
- # default condition
- return True
+
+ asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
+ fixed_asset_account = self.get_fixed_asset_account()
+
+ cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
+ cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
+
+ query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s"""
+ if asset_bought_with_invoice:
+ # with invoice purchase either expense or cwip has been booked
+ expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1)
+ if expense_booked:
+ # if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled
+ return False
+
+ cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
+ if cwip_booked:
+ # if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled
+ return True
+ else:
+ # with receipt purchase either cwip has been booked or no entries have been made
+ if not cwip_account:
+ # if cwip account isn't available do not make gl entries
+ return False
+
+ cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
+ # if cwip is not booked from receipt then do not make gl entries
+ # if cwip is booked from receipt then make gl entries
+ return cwip_booked
def get_purchase_document(self):
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
@@ -500,20 +508,25 @@ class Asset(AccountsController):
return purchase_document
- def get_asset_accounts(self):
- fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
- asset_category = self.asset_category, company = self.company)
+ def get_fixed_asset_account(self):
+ return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
+
+ def get_cwip_account(self, cwip_enabled=False):
+ cwip_account = None
+ try:
+ cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company)
+ except:
+ # if no cwip account found in category or company and "cwip is enabled" then raise else silently pass
+ if cwip_enabled:
+ raise
- cwip_account = get_asset_account("capital_work_in_progress_account",
- self.name, self.asset_category, self.company)
-
- return fixed_asset_account, cwip_account
+ return cwip_account
def make_gl_entries(self):
gl_entries = []
purchase_document = self.get_purchase_document()
- fixed_asset_account, cwip_account = self.get_asset_accounts()
+ fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index d914dabc123..89e1864cb61 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.assets.doctype.asset.asset import make_sales_invoice
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
class TestAsset(unittest.TestCase):
@@ -561,81 +562,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle)
- def test_gle_with_cwip_toggling(self):
- # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset
- frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
-
- pr = make_purchase_receipt(item_code="Macbook Pro",
- qty=1, rate=5000, do_not_submit=True, location="Test Location")
- pr.set('taxes', [{
- 'category': 'Total',
- 'add_deduct_tax': 'Add',
- 'charge_type': 'On Net Total',
- 'account_head': '_Test Account Service Tax - _TC',
- 'description': '_Test Account Service Tax',
- 'cost_center': 'Main - _TC',
- 'rate': 5.0
- }, {
- 'category': 'Valuation and Total',
- 'add_deduct_tax': 'Add',
- 'charge_type': 'On Net Total',
- 'account_head': '_Test Account Shipping Charges - _TC',
- 'description': '_Test Account Shipping Charges',
- 'cost_center': 'Main - _TC',
- 'rate': 5.0
- }])
- pr.submit()
- expected_gle = (
- ("Asset Received But Not Billed - _TC", 0.0, 5250.0),
- ("CWIP Account - _TC", 5250.0, 0.0)
- )
- pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type='Purchase Receipt' and voucher_no = %s
- order by account""", pr.name)
- self.assertEqual(pr_gle, expected_gle)
-
- pi = make_invoice(pr.name)
- pi.submit()
- expected_gle = (
- ("_Test Account Service Tax - _TC", 250.0, 0.0),
- ("_Test Account Shipping Charges - _TC", 250.0, 0.0),
- ("Asset Received But Not Billed - _TC", 5250.0, 0.0),
- ("Creditors - _TC", 0.0, 5500.0),
- ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
- )
- pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type='Purchase Invoice' and voucher_no = %s
- order by account""", pi.name)
- self.assertEqual(pi_gle, expected_gle)
-
- asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
- asset_doc = frappe.get_doc('Asset', asset)
- month_end_date = get_last_day(nowdate())
- asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
- self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
- asset_doc.append("finance_books", {
- "expected_value_after_useful_life": 200,
- "depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": month_end_date
- })
-
- # disable cwip and try submitting
- frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
- asset_doc.submit()
- # asset should have gl entries even if cwip is disabled
- expected_gle = (
- ("_Test Fixed Asset - _TC", 5250.0, 0.0),
- ("CWIP Account - _TC", 0.0, 5250.0)
- )
- gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type='Asset' and voucher_no = %s
- order by account""", asset_doc.name)
- self.assertEqual(gle, expected_gle)
-
- frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
-
def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location")
@@ -643,6 +569,74 @@ class TestAsset(unittest.TestCase):
doc = make_invoice(pr.name)
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
+
+ def test_asset_cwip_toggling_cases(self):
+ cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
+ name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"])
+ cwip_acc = "CWIP Account - _TC"
+
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
+ frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
+ frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "")
+
+ # case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set
+ pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
+ asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ asset_doc.available_for_use_date = nowdate()
+ asset_doc.calculate_depreciation = 0
+ asset_doc.submit()
+ gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
+ self.assertFalse(gle)
+
+ # case 1 -- PR with cwip disabled, Asset with cwip enabled
+ pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
+ frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
+ asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ asset_doc.available_for_use_date = nowdate()
+ asset_doc.calculate_depreciation = 0
+ asset_doc.submit()
+ gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
+ self.assertFalse(gle)
+
+ # case 2 -- PR with cwip enabled, Asset with cwip disabled
+ pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
+ asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ asset_doc.available_for_use_date = nowdate()
+ asset_doc.calculate_depreciation = 0
+ asset_doc.submit()
+ gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
+ self.assertTrue(gle)
+
+ # case 3 -- PI with cwip disabled, Asset with cwip enabled
+ pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
+ asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ asset_doc.available_for_use_date = nowdate()
+ asset_doc.calculate_depreciation = 0
+ asset_doc.submit()
+ gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
+ self.assertFalse(gle)
+
+ # case 4 -- PI with cwip enabled, Asset with cwip disabled
+ pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
+ asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ asset_doc.available_for_use_date = nowdate()
+ asset_doc.calculate_depreciation = 0
+ asset_doc.submit()
+ gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
+ self.assertTrue(gle)
+
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
+ frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
+ frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index 9a33fc14ac0..46620d56e98 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import cint
+from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
class AssetCategory(Document):
@@ -13,6 +13,7 @@ class AssetCategory(Document):
self.validate_finance_books()
self.validate_account_types()
self.validate_account_currency()
+ self.valide_cwip_account()
def validate_finance_books(self):
for d in self.finance_books:
@@ -58,6 +59,21 @@ class AssetCategory(Document):
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
title=_("Invalid Account"))
+
+ def valide_cwip_account(self):
+ if self.enable_cwip_accounting:
+ missing_cwip_accounts_for_company = []
+ for d in self.accounts:
+ if (not d.capital_work_in_progress_account and
+ not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")):
+ missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
+
+ if missing_cwip_accounts_for_company:
+ msg = _("""To enable Capital Work in Progress Accounting, """)
+ msg += _("""you must select Capital Work in Progress Account in accounts table""")
+ msg += "
"
+ msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company))
+ frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py
index b32f9b50202..39b79d6c507 100644
--- a/erpnext/assets/doctype/asset_category/test_asset_category.py
+++ b/erpnext/assets/doctype/asset_category/test_asset_category.py
@@ -26,4 +26,22 @@ class TestAssetCategory(unittest.TestCase):
asset_category.insert()
except frappe.DuplicateEntryError:
pass
-
\ No newline at end of file
+
+ def test_cwip_accounting(self):
+ company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account")
+ frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "")
+
+ asset_category = frappe.new_doc("Asset Category")
+ asset_category.asset_category_name = "Computers"
+ asset_category.enable_cwip_accounting = 1
+
+ asset_category.total_number_of_depreciations = 3
+ asset_category.frequency_of_depreciation = 3
+ asset_category.append("accounts", {
+ "company_name": "_Test Company",
+ "fixed_asset_account": "_Test Fixed Asset - _TC",
+ "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
+ "depreciation_expense_account": "_Test Depreciations - _TC"
+ })
+
+ self.assertRaises(frappe.ValidationError, asset_category.insert)
\ No newline at end of file
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index 88a865f0f85..beeca091c8a 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -143,7 +143,7 @@ def get_conditions(filters):
conditions = ""
if filters.get("company"):
- conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company'))
+ conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company'))
if filters.get("cost_center") or filters.get("project"):
conditions += """
@@ -151,10 +151,10 @@ def get_conditions(filters):
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
if filters.get("from_date"):
- conditions += " AND par.transaction_date>='%s'" % filters.get('from_date')
+ conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date')
if filters.get("to_date"):
- conditions += " AND par.transaction_date<='%s'" % filters.get('to_date')
+ conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
return conditions
def get_data(filters):
@@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
mr_records = {}
mr_details = frappe.db.sql("""
SELECT
- par.transaction_date,
- par.per_ordered,
- par.owner,
+ parent.transaction_date,
+ parent.per_ordered,
+ parent.owner,
child.name,
child.parent,
child.amount,
child.qty,
child.item_code,
child.uom,
- par.status
- FROM `tabMaterial Request` par, `tabMaterial Request Item` child
+ parent.status,
+ child.project,
+ child.cost_center
+ FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
WHERE
- par.per_ordered>=0
- AND par.name=child.parent
- AND par.docstatus=1
+ parent.per_ordered>=0
+ AND parent.name=child.parent
+ AND parent.docstatus=1
{conditions}
""".format(conditions=conditions), as_dict=1) #nosec
@@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
status=record.status,
actual_cost=0,
purchase_order_amt=0,
- purchase_order_amt_in_company_currency=0
+ purchase_order_amt_in_company_currency=0,
+ project = record.project,
+ cost_center = record.cost_center
)
procurement_record_against_mr.append(procurement_record_details)
return mr_records, procurement_record_against_mr
@@ -280,16 +284,16 @@ def get_po_entries(conditions):
child.amount,
child.base_amount,
child.schedule_date,
- par.transaction_date,
- par.supplier,
- par.status,
- par.owner
- FROM `tabPurchase Order` par, `tabPurchase Order Item` child
+ parent.transaction_date,
+ parent.supplier,
+ parent.status,
+ parent.owner
+ FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
WHERE
- par.docstatus = 1
- AND par.name = child.parent
- AND par.status not in ("Closed","Completed","Cancelled")
+ parent.docstatus = 1
+ AND parent.name = child.parent
+ AND parent.status not in ("Closed","Completed","Cancelled")
{conditions}
GROUP BY
- par.name, child.item_code
+ parent.name, child.item_code
""".format(conditions=conditions), as_dict=1) #nosec
\ No newline at end of file
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index c8f42a5921f..28bfb7a0072 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -629,22 +629,29 @@ class calculate_taxes_and_totals(object):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay):
- default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
- {'parent': self.doc.pos_profile, 'default': 1},
- ['mode_of_payment', 'type', 'account'], as_dict=1)
+ existing_amount = 0
- self.doc.payments = []
+ for payment in self.doc.payments:
+ existing_amount += payment.amount
- if default_mode_of_payment:
- self.doc.append('payments', {
- 'mode_of_payment': default_mode_of_payment.mode_of_payment,
- 'type': default_mode_of_payment.type,
- 'account': default_mode_of_payment.account,
- 'amount': total_amount_to_pay
- })
- else:
- self.doc.is_pos = 0
- self.doc.pos_profile = ''
+ # do not override user entered amount if equal to total_amount_to_pay
+ if existing_amount != total_amount_to_pay:
+ default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
+ {'parent': self.doc.pos_profile, 'default': 1},
+ ['mode_of_payment', 'type', 'account'], as_dict=1)
+
+ self.doc.payments = []
+
+ if default_mode_of_payment:
+ self.doc.append('payments', {
+ 'mode_of_payment': default_mode_of_payment.mode_of_payment,
+ 'type': default_mode_of_payment.type,
+ 'account': default_mode_of_payment.account,
+ 'amount': total_amount_to_pay
+ })
+ else:
+ self.doc.is_pos = 0
+ self.doc.pos_profile = ''
self.calculate_paid_amount()
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index c67330ad45f..2bf883809da 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -251,6 +251,10 @@ def make_bom(**args):
'rate': item_doc.valuation_rate or args.rate,
})
- bom.insert(ignore_permissions=True)
- bom.submit()
+ if not args.do_not_save:
+ bom.insert(ignore_permissions=True)
+
+ if not args.do_not_submit:
+ bom.submit()
+
return bom
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f917b098688..d82a4dd9fe8 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -371,6 +371,49 @@ class TestWorkOrder(unittest.TestCase):
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
self.assertEqual(len(ste1.items), 3)
+ def test_operation_time_with_batch_size(self):
+ fg_item = "Test Batch Size Item For BOM"
+ rm1 = "Test Batch Size Item RM 1 For BOM"
+
+ for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]:
+ make_item(item, {
+ "include_item_in_manufacturing": 1,
+ "is_stock_item": 1
+ })
+
+ bom_name = frappe.db.get_value("BOM",
+ {"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
+
+ if not bom_name:
+ bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
+ bom.with_operations = 1
+ bom.append("operations", {
+ "operation": "_Test Operation 1",
+ "workstation": "_Test Workstation 1",
+ "description": "Test Data",
+ "operating_cost": 100,
+ "time_in_mins": 40,
+ "batch_size": 5
+ })
+
+ bom.save()
+ bom.submit()
+ bom_name = bom.name
+
+ work_order = make_wo_order_test_record(item=fg_item,
+ planned_start_date=now(), qty=1, do_not_save=True)
+
+ work_order.set_work_order_operations()
+ work_order.save()
+ self.assertEqual(work_order.operations[0].time_in_mins, 8.0)
+
+ work_order1 = make_wo_order_test_record(item=fg_item,
+ planned_start_date=now(), qty=5, do_not_save=True)
+
+ work_order1.set_work_order_operations()
+ work_order1.save()
+ self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
+
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index d14c8d82f11..f9c028563bb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -611,7 +611,7 @@ erpnext.work_order = {
description: __('Max: {0}', [max]),
default: max
}, data => {
- max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
+ max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
if (data.qty > max) {
frappe.msgprint(__('Quantity must not be more than {0}', [max]));
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index b0585e5d734..603c8d4928c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -364,7 +364,7 @@ class WorkOrder(Document):
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) * math.ceil(flt(self.qty) / flt(d.batch_size))
+ d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost()
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 1e99aa3bf0b..8281bd98e4a 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -594,7 +594,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
$.each(actual_taxes_dict, function(key, value) {
if (value) total_actual_tax += value;
});
-
+
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
}
},
@@ -672,25 +672,33 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
);
}
- frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
- ['mode_of_payment', 'account', 'type'], (value) => {
- if (this.frm.is_dirty()) {
- frappe.model.clear_table(this.frm.doc, 'payments');
- if (value) {
- let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
- row.mode_of_payment = value.mode_of_payment;
- row.type = value.type;
- row.account = value.account;
- row.default = 1;
- row.amount = total_amount_to_pay;
- } else {
- this.frm.set_value('is_pos', 1);
- }
- this.frm.refresh_fields();
- }
- }, 'Sales Invoice');
+ let existing_amount = 0
+ $.each(this.frm.doc.payments || [], function(i, row) {
+ existing_amount += row.amount;
+ })
- this.calculate_paid_amount();
+ if (existing_amount != total_amount_to_pay) {
+ frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
+ ['mode_of_payment', 'account', 'type'], (value) => {
+ if (this.frm.is_dirty()) {
+ frappe.model.clear_table(this.frm.doc, 'payments');
+ if (value) {
+ let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
+ row.mode_of_payment = value.mode_of_payment;
+ row.type = value.type;
+ row.account = value.account;
+ row.default = 1;
+ row.amount = total_amount_to_pay;
+ } else {
+ this.frm.set_value('is_pos', 1);
+ }
+ this.frm.refresh_fields();
+ this.calculate_paid_amount();
+ }
+ }, 'Sales Invoice');
+ } else {
+ this.calculate_paid_amount();
+ }
},
set_default_payment: function(total_amount_to_pay, update_paid_amount) {
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index 8e39eb5037d..51b47c50a3b 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -50,16 +50,18 @@ class ItemPrice(Document):
def check_duplicates(self):
conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"
+ condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name)
for field in ['uom', 'valid_from',
'valid_upto', 'packing_unit', 'customer', 'supplier']:
if self.get(field):
conditions += " and {0} = %({1})s".format(field, field)
+ condition_data_dict[field] = self.get(field)
price_list_rate = frappe.db.sql("""
SELECT price_list_rate
FROM `tabItem Price`
- {conditions} """.format(conditions=conditions), self.as_dict())
+ {conditions} """.format(conditions=conditions), condition_data_dict)
if price_list_rate :
frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index ca59e67a676..003403a2f8a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -172,8 +172,9 @@ class StockReconciliation(StockController):
row.serial_no = ''
# item managed batch-wise not allowed
- if item.has_batch_no and not row.batch_no and not item.create_new_batch:
- raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code))
+ if item.has_batch_no and not row.batch_no and not frappe.flags.in_test:
+ if not item.create_new_batch or self.purpose != 'Opening Stock':
+ raise frappe.ValidationError(_("Batch no is required for the batched item {0}").format(item_code))
# docstatus should be < 2
validate_cancelled_item(item_code, item.docstatus, verbose=0)
@@ -191,10 +192,11 @@ class StockReconciliation(StockController):
serialized_items = False
for row in self.items:
item = frappe.get_cached_doc("Item", row.item_code)
- if not (item.has_serial_no or item.has_batch_no):
- if row.serial_no or row.batch_no:
+ if not (item.has_serial_no):
+ if row.serial_no:
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
.format(row.idx, frappe.bold(row.item_code)))
+
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
@@ -217,7 +219,12 @@ class StockReconciliation(StockController):
or (not previous_sle and not row.qty)):
continue
- sl_entries.append(self.get_sle_for_items(row))
+ sle_data = self.get_sle_for_items(row)
+
+ if row.batch_no:
+ sle_data.actual_qty = row.quantity_difference
+
+ sl_entries.append(sle_data)
else:
serialized_items = True
@@ -244,7 +251,7 @@ class StockReconciliation(StockController):
serial_nos = get_serial_nos(row.serial_no) or []
# To issue existing serial nos
- if row.current_qty and (row.current_serial_no or row.batch_no):
+ if row.current_qty and (row.current_serial_no):
args = self.get_sle_for_items(row)
args.update({
'actual_qty': -1 * row.current_qty,
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index a679c9415d0..8b073ec5ab4 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -361,6 +361,37 @@ class TestStockReconciliation(unittest.TestCase):
doc.cancel()
frappe.delete_doc(doc.doctype, doc.name)
+ def test_allow_negative_for_batch(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ item_code = "Stock-Reco-batch-Item-5"
+ warehouse = "_Test Warehouse for Stock Reco5 - _TC"
+
+ create_warehouse("_Test Warehouse for Stock Reco5", {"is_group": 0,
+ "parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"})
+
+ batch_item_doc = create_item(item_code, is_stock_item=1)
+ if not batch_item_doc.has_batch_no:
+ frappe.db.set_value("Item", item_code, {
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "Test-C.####"
+ })
+
+ ste1=make_stock_entry(posting_date="2020-10-07", posting_time="02:00", item_code=item_code,
+ target=warehouse, qty=2, basic_rate=100)
+
+ batch_no = ste1.items[0].batch_no
+
+ ste2=make_stock_entry(posting_date="2020-10-09", posting_time="02:00", item_code=item_code,
+ source=warehouse, qty=2, basic_rate=100, batch_no=batch_no)
+
+ sr = create_stock_reconciliation(item_code=item_code,
+ warehouse = warehouse, batch_no=batch_no, rate=200)
+
+ for doc in [sr, ste2, ste1]:
+ doc.cancel()
+ frappe.delete_doc(doc.doctype, doc.name)
+
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
index 84e95e27ca0..4204aee342b 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
@@ -3,6 +3,14 @@
frappe.query_reports["Batch-Wise Balance History"] = {
"filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ },
{
"fieldname":"from_date",
"label": __("From Date"),
@@ -18,6 +26,47 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"width": "80",
"default": frappe.datetime.get_today(),
"reqd": 1
- }
+ },
+ {
+ "fieldname":"item_code",
+ "label": __("Item Code"),
+ "fieldtype": "Link",
+ "options": "Item",
+ "get_query": function() {
+ return {
+ filters: {
+ "has_batch_no": 1
+ }
+ }
+ }
+ },
+ {
+ "fieldname":"warehouse",
+ "label": __("Warehouse"),
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "get_query": function() {
+ let company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: {
+ "company": company
+ }
+ }
+ }
+ },
+ {
+ "fieldname":"batch_no",
+ "label": __("Batch No"),
+ "fieldtype": "Link",
+ "options": "Batch",
+ "get_query": function() {
+ let item_code = frappe.query_report.get_filter_value('item_code');
+ return {
+ filters: {
+ "item": item_code
+ }
+ }
+ }
+ },
]
}
\ No newline at end of file
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index ec2ef35bb41..1999b7404e6 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -53,6 +53,10 @@ def get_conditions(filters):
else:
frappe.throw(_("'To Date' is required"))
+ for field in ["item_code", "warehouse", "batch_no", "company"]:
+ if filters.get(field):
+ conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field)))
+
return conditions
#get all details
diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py
index aa3ed92079c..12f32972039 100644
--- a/erpnext/stock/report/item_prices/item_prices.py
+++ b/erpnext/stock/report/item_prices/item_prices.py
@@ -77,38 +77,33 @@ def get_price_list():
return item_rate_map
def get_last_purchase_rate():
-
item_last_purchase_rate_map = {}
- query = """select * from (select
- result.item_code,
- result.base_rate
- from (
- (select
- po_item.item_code,
- po_item.item_name,
- po.transaction_date as posting_date,
- po_item.base_price_list_rate,
- po_item.discount_percentage,
- po_item.base_rate
- from `tabPurchase Order` po, `tabPurchase Order Item` po_item
- where po.name = po_item.parent and po.docstatus = 1)
- union
- (select
- pr_item.item_code,
- pr_item.item_name,
- pr.posting_date,
- pr_item.base_price_list_rate,
- pr_item.discount_percentage,
- pr_item.base_rate
- from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
- where pr.name = pr_item.parent and pr.docstatus = 1)
- ) result
- order by result.item_code asc, result.posting_date desc) result_wrapper
- group by item_code"""
+ query = """select * from (
+ (select
+ po_item.item_code,
+ po.transaction_date as posting_date,
+ po_item.base_rate
+ from `tabPurchase Order` po, `tabPurchase Order Item` po_item
+ where po.name = po_item.parent and po.docstatus = 1)
+ union
+ (select
+ pr_item.item_code,
+ pr.posting_date,
+ pr_item.base_rate
+ from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
+ where pr.name = pr_item.parent and pr.docstatus = 1)
+ union
+ (select
+ pi_item.item_code,
+ pi.posting_date,
+ pi_item.base_rate
+ from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item
+ where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1)
+ ) result order by result.item_code asc, result.posting_date asc"""
for d in frappe.db.sql(query, as_dict=1):
- item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate)
+ item_last_purchase_rate_map[d.item_code] = d.base_rate
return item_last_purchase_rate_map
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 6a265ec4cc5..67d3f233c81 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.utils import cint, flt
from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None):
include_uom = filters.get("include_uom")
@@ -23,6 +24,7 @@ def execute(filters=None):
actual_qty = stock_value = 0
+ available_serial_nos = {}
for sle in sl_entries:
item_detail = item_details[sle.item_code]
@@ -41,6 +43,9 @@ def execute(filters=None):
"stock_value": stock_value
})
+ if sle.serial_no:
+ update_available_serial_nos(available_serial_nos, sle)
+
data.append(sle)
if include_uom:
@@ -49,6 +54,27 @@ def execute(filters=None):
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data
+def update_available_serial_nos(available_serial_nos, sle):
+ serial_nos = get_serial_nos(sle.serial_no)
+ key = (sle.item_code, sle.warehouse)
+ if key not in available_serial_nos:
+ available_serial_nos.setdefault(key, [])
+
+ existing_serial_no = available_serial_nos[key]
+ for sn in serial_nos:
+ if sle.actual_qty > 0:
+ if sn in existing_serial_no:
+ existing_serial_no.remove(sn)
+ else:
+ existing_serial_no.append(sn)
+ else:
+ if sn in existing_serial_no:
+ existing_serial_no.remove(sn)
+ else:
+ existing_serial_no.append(sn)
+
+ sle.balance_serial_no = '\n'.join(existing_serial_no)
+
def get_columns():
columns = [
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95},
@@ -70,7 +96,8 @@ def get_columns():
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
- {"label": _("Serial #"), "fieldname": "serial_no", "width": 100},
+ {"label": _("Serial No"), "fieldname": "serial_no", "width": 100},
+ {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
]
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 5c4bba730e3..4fa080a2fd2 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -162,10 +162,13 @@ class update_entries_after(object):
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else:
- if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
- # assert
+ if sle.voucher_type=="Stock Reconciliation":
+ if sle.batch_no:
+ self.qty_after_transaction += flt(sle.actual_qty)
+ else:
+ self.qty_after_transaction = sle.qty_after_transaction
+
self.valuation_rate = sle.valuation_rate
- self.qty_after_transaction = sle.qty_after_transaction
self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else: