diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 69c3121653f..85293c15899 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '12.12.1'
+__version__ = '12.13.0'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index d2f7de3be52..70e6e44d719 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -97,7 +97,7 @@
"default": "1",
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check",
- "label": "Unlink Advance Payment on Cancelation of Order"
+ "label": "Unlink Advance Payment on Cancellation of Order"
},
{
"default": "1",
@@ -179,7 +179,7 @@
"icon": "icon-cog",
"idx": 1,
"issingle": 1,
- "modified": "2020-03-11 13:09:26.235848",
+ "modified": "2020-10-08 09:40:12.121145",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
index 4ca9f6b96fb..882955cd292 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
@@ -1,347 +1,122 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "field:year",
- "beta": 0,
- "creation": "2013-01-22 16:50:25",
- "custom": 0,
- "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "allow_import": 1,
+ "autoname": "field:year",
+ "creation": "2013-01-22 16:50:25",
+ "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "year",
+ "disabled",
+ "is_short_year",
+ "year_start_date",
+ "year_end_date",
+ "companies",
+ "auto_created"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "For e.g. 2012, 2012-13",
- "fieldname": "year",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Year Name",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "year",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "description": "For e.g. 2012, 2012-13",
+ "fieldname": "year",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Year Name",
+ "oldfieldname": "year",
+ "oldfieldtype": "Data",
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disabled",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "year_start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Year Start Date",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "year_start_date",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "year_start_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Year Start Date",
+ "no_copy": 1,
+ "oldfieldname": "year_start_date",
+ "oldfieldtype": "Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "year_end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Year End Date",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "year_end_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Year End Date",
+ "no_copy": 1,
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "companies",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Companies",
- "length": 0,
- "no_copy": 0,
- "options": "Fiscal Year Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "companies",
+ "fieldtype": "Table",
+ "label": "Companies",
+ "options": "Fiscal Year Company"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "fieldname": "auto_created",
- "fieldtype": "Check",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Auto Created",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "auto_created",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Auto Created",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "description": "Less than 12 months.",
+ "fieldname": "is_short_year",
+ "fieldtype": "Check",
+ "label": "Is Short Year",
+ "set_only_once": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-calendar",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-04-25 14:21:41.273354",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Fiscal Year",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-calendar",
+ "idx": 1,
+ "modified": "2020-10-03 18:22:04.161315",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Fiscal Year",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "read": 1,
+ "role": "Sales User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Purchase User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "read": 1,
+ "role": "Purchase User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "read": 1,
+ "role": "Accounts User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "read": 1,
+ "role": "Stock User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "read": 1,
+ "role": "Employee"
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 1,
- "sort_field": "name",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "name",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index d80bc7fad10..29c96bdae1e 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -36,6 +36,11 @@ class FiscalYear(Document):
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
def validate_dates(self):
+ if self.is_short_year:
+ # Fiscal Year can be shorter than one year, in some jurisdictions
+ # under certain circumstances. For example, in the USA and Germany.
+ return
+
if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate)
diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json
index d5723ca62ba..47be54f026f 100644
--- a/erpnext/accounts/doctype/fiscal_year/test_records.json
+++ b/erpnext/accounts/doctype/fiscal_year/test_records.json
@@ -58,5 +58,12 @@
"year": "_Test Fiscal Year 2021",
"year_end_date": "2021-12-31",
"year_start_date": "2021-01-01"
+ },
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Short Fiscal Year 2021",
+ "is_short_year": 1,
+ "year_end_date": "2021-12-31",
+ "year_start_date": "2021-04-01"
}
]
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/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index b35e32c5ca5..4be4c269f17 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -13,8 +13,7 @@ def get_data():
'Auto Repeat': 'reference_document',
},
'internal_links': {
- 'Sales Order': ['items', 'sales_order'],
- 'Delivery Note': ['items', 'delivery_note']
+ 'Sales Order': ['items', 'sales_order']
},
'transactions': [
{
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index bc34816e375..4ec2e65b2a3 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -263,13 +263,14 @@ class Subscription(Document):
invoice.set_taxes()
# Due date
- invoice.append(
- 'payment_schedule',
- {
- 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
- 'invoice_portion': 100
- }
- )
+ if self.days_until_due:
+ invoice.append(
+ 'payment_schedule',
+ {
+ 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
+ 'invoice_portion': 100
+ }
+ )
# Discounts
if self.additional_discount_percentage:
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index e38de252699..e11c0c39701 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -210,6 +210,7 @@ class TestSubscription(unittest.TestCase):
subscription.customer = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.start = '2018-01-01'
+ subscription.days_until_due = 1
subscription.insert()
subscription.process() # generate first invoice
diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
index bbbcc7f3a69..632e30db45d 100644
--- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
@@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template
+from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
+from erpnext.crm.doctype.opportunity.opportunity import make_quotation
test_records = frappe.get_test_records('Tax Rule')
@@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase):
self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
"_Test Sales Taxes and Charges Template 1 - _TC")
+ def test_taxes_fetch_via_tax_rule(self):
+ make_tax_rule(customer= "_Test Customer", billing_city = "_Test City",
+ sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+
+ # create opportunity for customer
+ opportunity = make_opportunity(with_items=1)
+
+ # make quotation from opportunity
+ quotation = make_quotation(opportunity.name)
+ quotation.save()
+
+ self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
+
+ # Check if accounts heads and rate fetched are also fetched from tax template or not
+ self.assertTrue(len(quotation.taxes) > 0)
+
+
def make_tax_rule(**args):
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index b539fff74e9..f6a7218d601 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
from `tabGL Entry`
where company = %s and
party in %s and fiscal_year=%s and credit > 0
+ and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries]
@@ -139,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
else:
tds_amount = _get_tds(net_total, tax_details.rate)
else:
- supplier_credit_amount = frappe.get_all('Purchase Invoice Item',
- fields = ['sum(net_amount)'],
- filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1)
+ supplier_credit_amount = frappe.get_all('Purchase Invoice',
+ fields = ['sum(net_total)'],
+ filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
@@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No
select distinct voucher_no
from `tabGL Entry`
where party in %s and %s and debit > 0
+ and is_opening = 'No'
""", (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index b1468999fc1..a0b0cbb9956 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
+ def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
+ invoices = []
+ frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ pi.submit()
+ invoices.append(pi)
+
+ # TDS not applied
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
+ pi.submit()
+ invoices.append(pi)
+
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ pi.submit()
+ invoices.append(pi)
+
+ self.assertEqual(pi.taxes_and_charges_deducted, 2000)
+ self.assertEqual(pi.grand_total, 8000)
+
+ # delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
def create_purchase_invoice(**args):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
@@ -109,7 +132,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc({
"doctype": "Purchase Invoice",
"posting_date": today(),
- "apply_tds": 1,
+ "apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier,
"company": '_Test Company',
"taxes_and_charges": "",
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
index efc76f9158b..97035278754 100644
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
+++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
@@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() {
const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
- if (r && r.enabled == "1") {
+ if (r && r.enabled === "1") {
me.plaid_status = "active"
} else {
me.plaid_status = "inactive"
@@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
}
make() {
- const me = this;
+ const me = this;
new frappe.ui.FileUploader({
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
allow_multiple: 0,
@@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() {
const me = this;
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
- .then(result => {
- me.plaid_env = result.plaid_env;
- me.plaid_public_key = result.plaid_public_key;
- me.client_name = result.client_name;
- me.sync_transactions()
- })
+ frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
+ .then(result => {
+ me.plaid_env = result.plaid_env;
+ me.client_name = result.client_name;
+ me.link_token = result.link_token;
+ me.sync_transactions();
+ })
}
sync_transactions() {
const me = this;
- frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => {
+ frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
- bank: v['bank'],
+ bank: r.bank,
bank_account: me.parent.bank_account,
freeze: true
})
.then((result) => {
- let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized")
+ let result_title = (result && result.length > 0)
+ ? __("{0} bank transaction(s) created", [result.length])
+ : __("This bank account is already synchronized");
+
let result_msg = `
-
-
${result_title}
- `
+
+
${result_title}
+ `
+
this.parent.$main_section.append(result_msg)
- frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'});
+ frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
})
})
}
@@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
})
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
- {bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")}
+ { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
).then((result) => {
me.make_dialog(result)
})
diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js
index cecf7f5e6fc..1e82e54cdf8 100755
--- a/erpnext/accounts/page/pos/pos.js
+++ b/erpnext/accounts/page/pos/pos.js
@@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$(frappe.render_template("pos_item", {
item_code: escape(obj.name),
item_price: item_price,
- title: obj.name || obj.item_name,
+ title: obj.name === obj.item_name ? obj.name : obj.item_name,
item_name: obj.name === obj.item_name ? "" : obj.item_name,
item_image: obj.image,
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
@@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$.each(this.frm.doc.items || [], function (i, d) {
$(frappe.render_template("pos_bill_item_new", {
item_code: escape(d.item_code),
- title: d.item_code || d.item_name,
+ title: d.item_code === d.item_name ? d.item_code : d.item_name,
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name),
qty: d.qty,
discount_percentage: d.discount_percentage || 0.0,
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index a49cc321495..65d13749d32 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -69,7 +69,7 @@ frappe.query_reports["Accounts Receivable"] = {
filters: {
'company': company
}
- }
+ };
}
},
{
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 04fc33220d9..f6632fa2632 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -617,9 +617,19 @@ class ReceivablePayableReport(object):
elif party_type_field=="supplier":
self.add_supplier_filters(conditions, values)
+ if self.filters.cost_center:
+ self.get_cost_center_conditions(conditions)
+
self.add_accounting_dimensions_filters(conditions, values)
return " and ".join(conditions), values
+ def get_cost_center_conditions(self, conditions):
+ lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
+ cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})]
+
+ cost_center_string = '", "'.join(cost_center_list)
+ conditions.append('cost_center in ("{0}")'.format(cost_center_string))
+
def get_order_by_condition(self):
if self.filters.get('group_by_party'):
return "order by party, posting_date"
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js
index 57fe4b05be4..8f028496cd5 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js
@@ -3,6 +3,14 @@
frappe.query_reports["Bank Reconciliation Statement"] = {
"filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
{
"fieldname":"account",
"label": __("Bank Account"),
@@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
"reqd": 1,
"get_query": function() {
+ var company = frappe.query_report.get_filter_value('company')
return {
"query": "erpnext.controllers.queries.get_account_list",
"filters": [
['Account', 'account_type', 'in', 'Bank, Cash'],
['Account', 'is_group', '=', 0],
+ ['Account', 'disabled', '=', 0],
+ ['Account', 'company', '=', company],
]
}
}
@@ -34,4 +45,4 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
"fieldtype": "Check"
},
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html
index 50947ecf5ef..2bb09cf0dc5 100644
--- a/erpnext/accounts/report/financial_statements.html
+++ b/erpnext/accounts/report/financial_statements.html
@@ -44,7 +44,7 @@
- {% for(let j=0, k=data.length-1; j
"
+ 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/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index c80f95e1555..79fcb957d4d 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -1,347 +1,99 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-05-08 14:44:37.095570",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-05-08 14:44:37.095570",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "finance_book",
+ "depreciation_method",
+ "total_number_of_depreciations",
+ "column_break_5",
+ "frequency_of_depreciation",
+ "depreciation_start_date",
+ "expected_value_after_useful_life",
+ "value_after_depreciation",
+ "rate_of_depreciation"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
- "fieldname": "finance_book",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Finance Book",
- "length": 0,
- "no_copy": 0,
- "options": "Finance Book",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "finance_book",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Finance Book",
+ "options": "Finance Book"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "depreciation_method",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Depreciation Method",
- "length": 0,
- "no_copy": 0,
- "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "depreciation_method",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Depreciation Method",
+ "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "total_number_of_depreciations",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Total Number of Depreciations",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "total_number_of_depreciations",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Total Number of Depreciations",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_5",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "frequency_of_depreciation",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Frequency of Depreciation (Months)",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "frequency_of_depreciation",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Frequency of Depreciation (Months)",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:parent.doctype == 'Asset'",
- "fetch_if_empty": 0,
- "fieldname": "depreciation_start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Depreciation Start Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:parent.doctype == 'Asset'",
+ "fieldname": "depreciation_start_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Depreciation Posting Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "eval:parent.doctype == 'Asset'",
- "fetch_if_empty": 0,
- "fieldname": "expected_value_after_useful_life",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expected Value After Useful Life",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:parent.doctype == 'Asset'",
+ "fieldname": "expected_value_after_useful_life",
+ "fieldtype": "Currency",
+ "label": "Expected Value After Useful Life",
+ "options": "Company:company:default_currency"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "value_after_depreciation",
- "fieldtype": "Currency",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Value After Depreciation",
- "length": 0,
- "no_copy": 1,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "value_after_depreciation",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Value After Depreciation",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
- "description": "In Percentage",
- "fetch_if_empty": 0,
- "fieldname": "rate_of_depreciation",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Rate of Depreciation",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
+ "description": "In Percentage",
+ "fieldname": "rate_of_depreciation",
+ "fieldtype": "Percent",
+ "label": "Rate of Depreciation"
}
- ],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-04-09 19:45:14.523488",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Asset Finance Book",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-09-16 12:11:30.631788",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Finance Book",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
index c3755a3fb9a..cddee5fa0f1 100644
--- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
@@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 10
})
if asset.docstatus == 0:
@@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 10
})
if asset.docstatus == 0:
asset.submit()
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 86e1e973abb..cb745d6dba8 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -202,6 +202,91 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
frappe.set_user("Administrator")
+ def test_update_child_with_tax_template(self):
+ """
+ Test Action: Create a PO with one item having its tax account head already in the PO.
+ Add the same item + new item with tax template via Update Items.
+ Expected result: First Item's tax row is updated. New tax row is added for second Item.
+ """
+ if not frappe.db.exists("Item", "Test Item with Tax"):
+ make_item("Test Item with Tax", {
+ 'is_stock_item': 1,
+ })
+
+ if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
+ frappe.get_doc({
+ 'doctype': 'Item Tax Template',
+ 'title': 'Test Update Items Template',
+ 'company': '_Test Company',
+ 'taxes': [
+ {
+ 'tax_type': "_Test Account Service Tax - _TC",
+ 'tax_rate': 10,
+ }
+ ]
+ }).insert()
+
+ new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
+
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
+
+ tax_template = "_Test Account Excise Duty @ 10"
+ item = "_Test Item Home Desktop 100"
+ if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
+ item_doc = frappe.get_doc("Item", item)
+ item_doc.append("taxes", {
+ "item_tax_template": tax_template,
+ "valid_from": nowdate()
+ })
+ item_doc.save()
+ else:
+ # update valid from
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
+ where parent = %(item)s and item_tax_template = %(tax)s""",
+ {"item": item, "tax": tax_template})
+
+ po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
+
+ po.append("taxes", {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Purchase Taxes and Charges",
+ "rate": 10
+ })
+ po.insert()
+ po.submit()
+
+ self.assertEqual(po.taxes[0].tax_amount, 50)
+ self.assertEqual(po.taxes[0].total, 550)
+
+ items = json.dumps([
+ {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
+ {'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO
+ {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
+ ])
+ update_child_qty_rate('Purchase Order', items, po.name)
+
+ po.reload()
+ self.assertEqual(po.taxes[0].tax_amount, 70)
+ self.assertEqual(po.taxes[0].total, 770)
+ self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC")
+ self.assertEqual(po.taxes[1].tax_amount, 70)
+ self.assertEqual(po.taxes[1].total, 840)
+
+ # teardown
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
+ where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
+ po.cancel()
+ po.delete()
+ new_item_with_tax.delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+
def test_update_qty(self):
po = create_purchase_order()
@@ -733,6 +818,59 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM")
+ def test_supplied_qty_against_subcontracted_po(self):
+ item_code = "_Test Subcontracted FG Item 5"
+ make_item('Sub Contracted Raw Material 4', {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1
+ })
+
+ make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
+
+ update_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 250
+ po = create_purchase_order(item_code=item_code, qty=order_qty,
+ is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
+
+ # Add same subcontracted items multiple times
+ po.append("items", {
+ "item_code": item_code,
+ "qty": order_qty,
+ "schedule_date": add_days(nowdate(), 1),
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ po.set_missing_values()
+ po.submit()
+
+ # Material receipt entry for the raw materials which will be send to supplier
+ make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
+
+ rm_items = [
+ {
+ "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
+ "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
+ },
+ {
+ "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
+ "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
+ },
+ ]
+
+ # Raw Materials transfer entry from stores to supplier's warehouse
+ rm_item_string = json.dumps(rm_items)
+ se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
+ se.submit()
+
+ po_doc = frappe.get_doc("Purchase Order", po.name)
+ for row in po_doc.supplied_items:
+ # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
+ self.assertEqual(row.supplied_qty, 250.0)
+
+ update_backflush_based_on("BOM")
+
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings",
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/change_log/v12/v12_13_0.md b/erpnext/change_log/v12/v12_13_0.md
new file mode 100644
index 00000000000..73054e90cc8
--- /dev/null
+++ b/erpnext/change_log/v12/v12_13_0.md
@@ -0,0 +1,51 @@
+## ERPNext v12.13.0 Release Note
+
+### Fixes and Enhancements
+
+- Warehouse address filtered based on warehouse ([#23381](https://github.com/frappe/erpnext/pull/23381))
+- COGS validation in the purchase receipt ([#23536](https://github.com/frappe/erpnext/pull/23536))
+- Item Tax Updating via `Update Items` in SO/PO ([#23338](https://github.com/frappe/erpnext/pull/23338))
+- Pricing rule selector is wrong ([#22328](https://github.com/frappe/erpnext/pull/22328))
+- Adding filters validation Batch-Wise Balance History ([#23396](https://github.com/frappe/erpnext/pull/23396))
+- Add company and correct filter in bank statement reconciliation report filters ([#23618](https://github.com/frappe/erpnext/pull/23618))
+- Incorrect consumed qty if raw material with batch ([#23389](https://github.com/frappe/erpnext/pull/23389))
+- Balance serial nos in stock ledger report ([#23520](https://github.com/frappe/erpnext/pull/23520))
+- Use Plaid's new API (v12) ([#23317](https://github.com/frappe/erpnext/pull/23317))
+- Cost Center filter in accounts receivable and payable report ([#23356](https://github.com/frappe/erpnext/pull/23356))
+- Incorrect operation time calculation for batch size ([#23480](https://github.com/frappe/erpnext/pull/23480))
+- Serial no field is blank in stock reconciliation ([#23648](https://github.com/frappe/erpnext/pull/23648))
+- Manually set serial nos override with current available serial nos ([#23657](https://github.com/frappe/erpnext/pull/23657))
+- Can't save item price after adding child table ([#23594](https://github.com/frappe/erpnext/pull/23594))
+- Naming series - cannot reset current value to zero ([#23505](https://github.com/frappe/erpnext/pull/23505))
+- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23463](https://github.com/frappe/erpnext/pull/23463))
+- Taxes not getting fetched from Opportunity to Quotation ([#23354](https://github.com/frappe/erpnext/pull/23354))
+- Stock reconciliation, incorrect serial nos fetched in the current serial no field ([#23366](https://github.com/frappe/erpnext/pull/23366))
+- Cannot create asset if cwip disabled and account not set ([#23584](https://github.com/frappe/erpnext/pull/23584))
+- Leave application status fix ([#23411](https://github.com/frappe/erpnext/pull/23411))
+- Not able to do overproduction ([#23600](https://github.com/frappe/erpnext/pull/23600))
+- Incorrect supplied qty error ([#23619](https://github.com/frappe/erpnext/pull/23619))
+- Download Required Materials not working for production plan ([#23403](https://github.com/frappe/erpnext/pull/23403))
+- Make account number length configurable ([#23496](https://github.com/frappe/erpnext/pull/23496))
+- Include item_code in items result to allow adding product info in custom templates ([#23440](https://github.com/frappe/erpnext/pull/23440))
+- Mode of payment getting overwritten by default mode of payment for returns ([#23599](https://github.com/frappe/erpnext/pull/23599))
+- Show total row in print format of financial statement ([#23565](https://github.com/frappe/erpnext/pull/23565))
+- Performance issue while adding template item in the cart ([#23508](https://github.com/frappe/erpnext/pull/23508))
+- Display item name instead of item code in offline POS ([#23451](https://github.com/frappe/erpnext/pull/23451))
+- Set stock UOM in item while creating material request from stock entry ([#23430](https://github.com/frappe/erpnext/pull/23430))
+- Book loss amount in the COGS instead of stock received but not billed ([#23412](https://github.com/frappe/erpnext/pull/23412))
+- Depreciation start date ux fixes ([#23340](https://github.com/frappe/erpnext/pull/23340))
+- Payment Schedule not fetching ([#23477](https://github.com/frappe/erpnext/pull/23477))
+- Taxable value in GSTR 3B report ([#23552](https://github.com/frappe/erpnext/pull/23552))
+- Negative stock error while submitting stock reco for batch item ([#23564](https://github.com/frappe/erpnext/pull/23564))
+- Check only Read and Write Permission in Update Items ([#23458](https://github.com/frappe/erpnext/pull/23458))
+- Handle Blank from/to range in Numeric Item Attribute ([#23484](https://github.com/frappe/erpnext/pull/23484))
+- Added new filters in the Batch-wise balance history report ([#23522](https://github.com/frappe/erpnext/pull/23522))
+- Added filter show in website for filtering product ([#23638](https://github.com/frappe/erpnext/pull/23638))
+- Enabled no copy property for Supplier Invoice Date to avoid due date validation ([#23367](https://github.com/frappe/erpnext/pull/23367))
+- Showing a negative balance in expired leaves ([#23426](https://github.com/frappe/erpnext/pull/23426))
+- Validate if removed item attributes exist in variants ([#23591](https://github.com/frappe/erpnext/pull/23591))
+- Longer timeout for company replace abbreviation ([#23442](https://github.com/frappe/erpnext/pull/23442))
+- Online pos print not working ([#23377](https://github.com/frappe/erpnext/pull/23377))
+- Last purchase rate in item prices report ([#23507](https://github.com/frappe/erpnext/pull/23507))
+- Do not consider opening entries for TDS calculation ([#23598](https://github.com/frappe/erpnext/pull/23598))
+- Project value is missing from procurement-tracker ([#23551](https://github.com/frappe/erpnext/pull/23551))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index ca62ec78cce..31045a97671 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -20,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t
from erpnext.exceptions import InvalidCurrency
from six import text_type
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
-from erpnext.stock.get_item_details import get_item_warehouse
+from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
@@ -1127,6 +1127,43 @@ def get_supplier_block_status(party_name):
}
return info
+def set_child_tax_template_and_map(item, child_item, parent_doc):
+ args = {
+ 'item_code': item.item_code,
+ 'posting_date': parent_doc.transaction_date,
+ 'tax_category': parent_doc.get('tax_category'),
+ 'company': parent_doc.get('company')
+ }
+
+ child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
+ if child_item.get("item_tax_template"):
+ child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
+
+def add_taxes_from_tax_template(child_item, parent_doc):
+ add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
+
+ if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
+ tax_map = json.loads(child_item.get("item_tax_rate"))
+ for tax_type in tax_map:
+ tax_rate = flt(tax_map[tax_type])
+ taxes = parent_doc.get('taxes') or []
+ # add new row for tax head only if missing
+ found = any(tax.account_head == tax_type for tax in taxes)
+ if not found:
+ tax_row = parent_doc.append("taxes", {})
+ tax_row.update({
+ "description" : str(tax_type).split(' - ')[0],
+ "charge_type" : "On Net Total",
+ "account_head" : tax_type,
+ "rate" : tax_rate
+ })
+ if parent_doc.doctype == "Purchase Order":
+ tax_row.update({
+ "category" : "Total",
+ "add_deduct_tax" : "Add"
+ })
+ tax_row.db_insert()
+
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
"""
Returns a Sales Order Item child item containing the default values
@@ -1140,6 +1177,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.uom = item.stock_uom
+ set_child_tax_template_and_map(item, child_item, p_doc)
+ add_taxes_from_tax_template(child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@@ -1162,6 +1201,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.uom = item.stock_uom
child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation
+ set_child_tax_template_and_map(item, child_item, p_doc)
+ add_taxes_from_tax_template(child_item, p_doc)
return child_item
def validate_and_delete_children(parent, data):
@@ -1270,19 +1311,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
validate_quantity(child_item, d)
child_item.qty = flt(d.get("qty"))
- precision = child_item.precision("rate") or 2
+ rate_precision = child_item.precision("rate") or 2
+ conv_fac_precision = child_item.precision("conversion_factor") or 2
+ qty_precision = child_item.precision("qty") or 2
- if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision):
+ if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision):
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
.format(child_item.idx, child_item.item_code))
else:
- child_item.rate = flt(d.get("rate"))
+ child_item.rate = flt(d.get("rate"), rate_precision)
if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1
else:
- child_item.conversion_factor = flt(d.get('conversion_factor'))
+ child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision)
if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date')
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 1399654ffd2..3ebb12541ab 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -10,6 +10,7 @@ from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.doctype.item.item import set_item_default
from frappe.contacts.doctype.address.address import get_address_display
+from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController
@@ -53,10 +54,10 @@ class SellingController(StockController):
super(SellingController, self).set_missing_values(for_validate)
# set contact and address details for customer, if they are not mentioned
- self.set_missing_lead_customer_details()
+ self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
- def set_missing_lead_customer_details(self):
+ def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
if getattr(self, "customer", None):
customer = self.customer
@@ -93,6 +94,11 @@ class SellingController(StockController):
posting_date=self.get('transaction_date') or self.get('posting_date'),
company=self.company))
+ if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate:
+ taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges)
+ for tax in taxes:
+ self.append('taxes', tax)
+
def set_price_list_and_item_details(self, for_validate=False):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
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/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 532e19cffd9..a033a2a722d 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -2,81 +2,90 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-from frappe import _
-from frappe.utils.password import get_decrypted_password
-from plaid import Client
-from plaid.errors import APIError, ItemError
+import plaid
+import requests
+from plaid.errors import APIError, ItemError, InvalidRequestError
import frappe
-import requests
+from frappe import _
+
class PlaidConnector():
def __init__(self, access_token=None):
-
- plaid_settings = frappe.get_single("Plaid Settings")
-
- self.config = {
- "plaid_client_id": plaid_settings.plaid_client_id,
- "plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
- "plaid_public_key": plaid_settings.plaid_public_key,
- "plaid_env": plaid_settings.plaid_env
- }
-
- self.client = Client(client_id=self.config.get("plaid_client_id"),
- secret=self.config.get("plaid_secret"),
- public_key=self.config.get("plaid_public_key"),
- environment=self.config.get("plaid_env")
- )
-
self.access_token = access_token
+ self.settings = frappe.get_single("Plaid Settings")
+ self.products = ["auth", "transactions"]
+ self.client_name = frappe.local.site
+ self.client = plaid.Client(
+ client_id=self.settings.plaid_client_id,
+ secret=self.settings.get_password("plaid_secret"),
+ environment=self.settings.plaid_env,
+ api_version="2019-05-29"
+ )
def get_access_token(self, public_token):
if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
-
response = self.client.Item.public_token.exchange(public_token)
- access_token = response['access_token']
-
+ access_token = response["access_token"]
return access_token
+ def get_link_token(self):
+ token_request = {
+ "client_name": self.client_name,
+ "client_id": self.settings.plaid_client_id,
+ "secret": self.settings.plaid_secret,
+ "products": self.products,
+ # only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
+ "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
+ "country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
+ "user": {
+ "client_user_id": frappe.generate_hash(frappe.session.user, length=32)
+ }
+ }
+
+ try:
+ response = self.client.LinkToken.create(token_request)
+ except InvalidRequestError:
+ frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
+ frappe.msgprint(_("Please check your Plaid client ID and secret values"))
+ except APIError as e:
+ frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
+ frappe.throw(_(str(e)), title=_("Authentication Failed"))
+ else:
+ return response["link_token"]
+
def auth(self):
try:
self.client.Auth.get(self.access_token)
- print("Authentication successful.....")
except ItemError as e:
- if e.code == 'ITEM_LOGIN_REQUIRED':
- pass
- else:
+ if e.code == "ITEM_LOGIN_REQUIRED":
pass
except APIError as e:
- if e.code == 'PLANNED_MAINTENANCE':
- pass
- else:
+ if e.code == "PLANNED_MAINTENANCE":
pass
except requests.Timeout:
pass
except Exception as e:
- print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
- frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'})
+ frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None):
+ self.auth()
+ kwargs = dict(
+ access_token=self.access_token,
+ start_date=start_date,
+ end_date=end_date
+ )
+ if account_id:
+ kwargs.update(dict(account_ids=[account_id]))
+
try:
- self.auth()
- if account_id:
- account_ids = [account_id]
-
- response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
-
- else:
- response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
-
- transactions = response['transactions']
-
- while len(transactions) < response['total_transactions']:
+ response = self.client.Transactions.get(**kwargs)
+ transactions = response["transactions"]
+ while len(transactions) < response["total_transactions"]:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
- transactions.extend(response['transactions'])
+ transactions.extend(response["transactions"])
return transactions
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 0ffbb877ea7..22a4004955f 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -4,14 +4,14 @@
frappe.provide("erpnext.integrations");
frappe.ui.form.on('Plaid Settings', {
- enabled: function(frm) {
+ enabled: function (frm) {
frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
frm.toggle_reqd('plaid_secret', frm.doc.enabled);
- frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
frm.toggle_reqd('plaid_env', frm.doc.enabled);
},
- refresh: function(frm) {
- if(frm.doc.enabled) {
+
+ refresh: function (frm) {
+ if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => {
new erpnext.integrations.plaidLink(frm);
});
@@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) {
this.frm = parent;
- this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config();
}
- init_config() {
- const me = this;
- me.plaid_env = me.frm.doc.plaid_env;
- me.plaid_public_key = me.frm.doc.plaid_public_key;
- me.client_name = frappe.boot.sitename;
- me.init_plaid();
+ async init_config() {
+ this.product = ["auth", "transactions"];
+ this.plaid_env = this.frm.doc.plaid_env;
+ this.client_name = frappe.boot.sitename;
+ this.token = await this.frm.call("get_link_token").then(resp => resp.message);
+ this.init_plaid();
}
init_plaid() {
@@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
}
onScriptLoaded(me) {
- me.linkHandler = window.Plaid.create({
+ me.linkHandler = Plaid.create({
clientName: me.client_name,
+ product: me.product,
env: me.plaid_env,
- key: me.plaid_public_key,
- onSuccess: me.plaid_success,
- product: me.product
+ token: me.token,
+ onSuccess: me.plaid_success
});
}
onScriptError(error) {
- frappe.msgprint('There was an issue loading the link-initialize.js script');
+ frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error);
}
@@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink {
const me = this;
frappe.prompt({
- fieldtype:"Link",
+ fieldtype: "Link",
options: "Company",
- label:__("Company"),
- fieldname:"company",
- reqd:1
+ label: __("Company"),
+ fieldname: "company",
+ reqd: 1
}, (data) => {
me.company = data.company;
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response})
- .then((result) => {
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response,
- bank: result, company: me.company});
- })
- .then(() => {
- frappe.show_alert({message:__("Bank accounts added"), indicator:'green'});
+ frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
+ token: token,
+ response: response
+ }).then((result) => {
+ frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
+ response: response,
+ bank: result,
+ company: me.company
});
+ }).then(() => {
+ frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
+ });
}, __("Select a company"), __("Continue"));
}
};
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
index d8203d7390f..27062172239 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
@@ -1,5 +1,4 @@
{
- "actions": [],
"creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType",
"editable_grid": 1,
@@ -12,7 +11,6 @@
"plaid_client_id",
"plaid_secret",
"column_break_7",
- "plaid_public_key",
"plaid_env"
],
"fields": [
@@ -41,12 +39,6 @@
"in_list_view": 1,
"label": "Plaid Secret"
},
- {
- "fieldname": "plaid_public_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Plaid Public Key"
- },
{
"fieldname": "plaid_env",
"fieldtype": "Select",
@@ -69,8 +61,7 @@
}
],
"issingle": 1,
- "links": [],
- "modified": "2020-02-07 15:21:11.616231",
+ "modified": "2020-09-12 02:31:44.542385",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 81fb9843f61..3afccf95b8e 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -2,30 +2,36 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import frappe
import json
-from frappe import _
-from frappe.model.document import Document
+
+import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
-from frappe.utils import getdate, formatdate, today, add_months
+from frappe import _
from frappe.desk.doctype.tag.tag import add_tag
+from frappe.model.document import Document
+from frappe.utils import add_months, formatdate, getdate, today
+
class PlaidSettings(Document):
- pass
+ @staticmethod
+ def get_link_token():
+ plaid = PlaidConnector()
+ return plaid.get_link_token()
+
@frappe.whitelist()
-def plaid_configuration():
+def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings")
return {
- "plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env,
+ "link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site
}
- else:
- return "disabled"
+
+ return "disabled"
+
@frappe.whitelist()
def add_institution(token, response):
@@ -33,6 +39,7 @@ def add_institution(token, response):
plaid = PlaidConnector()
access_token = plaid.get_access_token(token)
+ bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]):
try:
@@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert()
except Exception:
frappe.throw(frappe.get_traceback())
-
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@@ -52,6 +58,7 @@ def add_institution(token, response):
return bank
+
@frappe.whitelist()
def add_bank_accounts(response, bank, company):
try:
@@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert()
result.append(new_account.name)
-
except frappe.UniqueValidationError:
- frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name))
+ frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception:
frappe.throw(frappe.get_traceback())
@@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result
+
def add_account_type(account_type):
try:
frappe.get_doc({
@@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception:
frappe.throw(frappe.get_traceback())
+
@frappe.whitelist()
def sync_transactions(bank, bank_account):
- ''' Sync transactions based on the last integration date as the start date, after sync is completed
- add the transaction date of the oldest transaction as the last integration date '''
+ """Sync transactions based on the last integration date as the start date, after sync is completed
+ add the transaction date of the oldest transaction as the last integration date."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date:
@@ -148,10 +156,10 @@ def sync_transactions(bank, bank_account):
len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
-
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
+
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None
@@ -169,6 +177,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions
+
def new_bank_transaction(transaction):
result = []
@@ -183,8 +192,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled"
+ tags = []
try:
- tags = []
tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError:
@@ -217,6 +226,7 @@ def new_bank_transaction(transaction):
return result
+
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@@ -224,4 +234,8 @@ def automatic_synchronization():
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts:
- frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name)
+ frappe.enqueue(
+ "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
+ bank=plaid_account.bank,
+ bank_account=plaid_account.name
+ )
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index 29e8fa4fec8..9f0bb92f537 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-import unittest
-import frappe
-from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json
-from frappe.utils.response import json_handler
+import unittest
+
+import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
+from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
+ add_account_subtype, add_account_type, add_bank_accounts,
+ new_bank_transaction, get_plaid_configuration)
+from frappe.utils.response import json_handler
+
class TestPlaidSettings(unittest.TestCase):
def setUp(self):
@@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
- self.assertTrue(plaid_configuration() == "disabled")
+ self.assertTrue(get_plaid_configuration() == "disabled")
def test_add_account_type(self):
add_account_type("brokerage")
@@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
- }],
+ }],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
- }],
+ }],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase):
new_bank_transaction(transactions)
- self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)
\ No newline at end of file
+ self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py
index 8c7b6f723f9..bfd882e532a 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.py
@@ -1,3 +1,4 @@
+
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
@@ -31,7 +32,7 @@ class HolidayList(Document):
def validate_days(self):
- if self.from_date > self.to_date:
+ if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date"))
for day in self.get("holidays"):
diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json
index 9271943a348..ee85a8437ee 100644
--- a/erpnext/hr/doctype/leave_application/leave_application.json
+++ b/erpnext/hr/doctype/leave_application/leave_application.json
@@ -244,9 +244,10 @@
],
"icon": "fa fa-calendar",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"max_attachments": 3,
- "modified": "2020-09-23 18:53:11.608446",
+ "modified": "2020-09-23 19:11:58.806837",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Application",
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/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py
index 8e60d33f850..d2bcb12070c 100644
--- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py
+++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py
@@ -4,17 +4,16 @@
from __future__ import unicode_literals
import frappe
+
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
plaid_settings = frappe.get_single("Plaid Settings")
if plaid_settings.enabled:
- if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \
- and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
+ if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
plaid_settings.enabled = 0
else:
plaid_settings.update({
"plaid_client_id": frappe.conf.plaid_client_id,
- "plaid_public_key": frappe.conf.plaid_public_key,
"plaid_env": frappe.conf.plaid_env,
"plaid_secret": frappe.conf.plaid_secret
})
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index c8d3203cb70..c3fa6d797cb 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -14,13 +14,15 @@ def get_field_filter_data():
for f in fields:
doctype = f.get_link_doctype()
- # apply enable/disable filter
+ # apply enable/disable/show_in_website filter
meta = frappe.get_meta(doctype)
filters = {}
if meta.has_field('enabled'):
filters['enabled'] = 1
if meta.has_field('disabled'):
filters['disabled'] = 0
+ if meta.has_field('show_in_website'):
+ filters['show_in_website'] = 1
values = [d.name for d in frappe.get_all(doctype, filters)]
filter_data.append([f, values])
@@ -378,7 +380,7 @@ def get_items(filters=None, search=None):
results = frappe.db.sql('''
SELECT
- `tabItem`.`name`, `tabItem`.`item_name`,
+ `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
`tabItem`.`website_image`, `tabItem`.`image`,
`tabItem`.`web_long_description`, `tabItem`.`description`,
`tabItem`.`route`
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/public/js/utils.js b/erpnext/public/js/utils.js
index 9b1c94e5ba0..264344ca94c 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) {
const frm = opts.frm;
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
+ const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
+ const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
+
this.data = [];
const fields = [{
fieldtype:'Data',
@@ -472,14 +475,16 @@ erpnext.utils.update_child_items = function(opts) {
default: 0,
read_only: 0,
in_list_view: 1,
- label: __('Qty')
+ label: __('Qty'),
+ precision: get_precision("qty")
}, {
fieldtype:'Currency',
fieldname:"rate",
default: 0,
read_only: 0,
in_list_view: 1,
- label: __('Rate')
+ label: __('Rate'),
+ precision: get_precision("rate")
}];
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
@@ -494,7 +499,8 @@ erpnext.utils.update_child_items = function(opts) {
fieldtype: 'Float',
fieldname: "conversion_factor",
in_list_view: 1,
- label: __("Conversion Factor")
+ label: __("Conversion Factor"),
+ precision: get_precision('conversion_factor')
})
}
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
index cf2644e0053..ac876229ecb 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
@@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', {
}
};
});
+
+ frm.set_query('parent_quality_procedure', function(){
+ return {
+ filters: {
+ is_group: 1
+ }
+ };
+ });
}
});
\ No newline at end of file
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
index b3c0d948909..32a1ebcc0ca 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
@@ -21,8 +21,7 @@
"fieldname": "parent_quality_procedure",
"fieldtype": "Link",
"label": "Parent Procedure",
- "options": "Quality Procedure",
- "read_only": 1
+ "options": "Quality Procedure"
},
{
"default": "0",
@@ -73,7 +72,7 @@
],
"is_tree": 1,
"links": [],
- "modified": "2020-06-17 17:25:03.434953",
+ "modified": "2020-10-12 16:14:11.167537",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Procedure",
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
index 1952e578673..797c26b64c2 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils.nestedset import NestedSet
+from frappe.utils.nestedset import NestedSet, rebuild_tree
from frappe import _
class QualityProcedure(NestedSet):
@@ -42,6 +42,8 @@ class QualityProcedure(NestedSet):
doc.save(ignore_permissions=True)
def set_parent(self):
+ rebuild_tree('Quality Procedure', 'parent_quality_procedure')
+
for process in self.processes:
# Set parent for only those children who don't have a parent
parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
index caed7367dc1..d319324a68e 100644
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ b/erpnext/regional/doctype/datev_settings/datev_settings.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"client",
+ "account_number_length",
"column_break_2",
"client_number",
"section_break_4",
@@ -57,9 +58,16 @@
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "4",
+ "fieldname": "account_number_length",
+ "fieldtype": "Int",
+ "label": "Account Number Length",
+ "reqd": 1
}
],
- "modified": "2020-04-15 12:59:57.786506",
+ "modified": "2020-10-03 16:52:35.903867",
"modified_by": "Administrator",
"module": "Regional",
"name": "DATEV Settings",
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index b02c4bc7333..25c30acba76 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -158,7 +158,7 @@ class GSTR3BReport(Document):
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
- self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
+ self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas", "Registered Regular"], reverse_charge="Y")
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
self.set_itc_details(itc_details)
@@ -197,7 +197,7 @@ class GSTR3BReport(Document):
if d["ty"] == 'ISRC':
reverse_charge = "Y"
itc_type = 'All Other ITC'
- gst_category = ['Unregistered', 'Overseas']
+ gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
else:
reverse_charge = "N"
@@ -253,7 +253,7 @@ class GSTR3BReport(Document):
def get_total_taxable_value(self, doctype, reverse_charge):
return frappe._dict(frappe.db.sql("""
- select gst_category, sum(net_total) as total
+ select gst_category, sum(base_net_total) as total
from `tab{doctype}`
where docstatus = 1 and month(posting_date) = %s
and year(posting_date) = %s and reverse_charge = %s
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index a2d74b889ad..9f2e86bab36 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -16,6 +16,7 @@ from csv import QUOTE_NONNUMERIC
import frappe
from frappe import _
+from erpnext.accounts.utils import get_fiscal_year
import pandas as pd
@@ -30,20 +31,33 @@ def execute(filters=None):
def validate(filters):
"""Make sure all mandatory filters and settings are present."""
- if not filters.get('company'):
+ company = filters.get('company')
+ if not company:
frappe.throw(_('Company is a mandatory filter.'))
- if not filters.get('from_date'):
+ from_date = filters.get('from_date')
+ if not from_date:
frappe.throw(_('From Date is a mandatory filter.'))
- if not filters.get('to_date'):
+ to_date = filters.get('to_date')
+ if not to_date:
frappe.throw(_('To Date is a mandatory filter.'))
+ validate_fiscal_year(from_date, to_date, company)
+
try:
frappe.get_doc('DATEV Settings', filters.get('company'))
except frappe.DoesNotExistError:
frappe.throw(_('Please create DATEV Settings for Company {}.').format(filters.get('company')))
+
+def validate_fiscal_year(from_date, to_date, company):
+ from_fiscal_year = get_fiscal_year(date=from_date, company=company)
+ to_fiscal_year = get_fiscal_year(date=to_date, company=company)
+ if from_fiscal_year != to_fiscal_year:
+ frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date))
+
+
def get_columns():
"""Return the list of columns that will be shown in query report."""
columns = [
@@ -231,9 +245,9 @@ def get_datev_csv(data, filters):
# L = Tax client number (Mandantennummer)
frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "",
# M = Start of the fiscal year (Wirtschaftsjahresbeginn)
- frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"),
+ frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"),
# N = Length of account numbers (Sachkontenlänge)
- "4",
+ str(filters.get('account_number_length', 4)),
# O = Transaction batch start date (YYYYMMDD)
frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"),
# P = Transaction batch end date (YYYYMMDD)
@@ -507,6 +521,12 @@ def download_datev_csv(filters=None):
filters = json.loads(filters)
validate(filters)
+
+ filters['account_number_length'] = frappe.get_value('DATEV Settings', filters.get('company'), 'account_number_length')
+
+ fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=filters.get('company'))
+ filters['fiscal_year_start'] = fiscal_year[1]
+
data = get_gl_entries(filters, as_dict=1)
frappe.response['result'] = get_datev_csv(data, filters)
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 1484f6b2290..a4a63d2aef2 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -129,7 +129,7 @@ class Customer(TransactionBase):
address = frappe.get_doc('Address', address_name.get('name'))
if not address.has_link('Customer', self.name):
address.append('links', dict(link_doctype='Customer', link_name=self.name))
- address.save()
+ address.save(ignore_permissions=self.flags.ignore_permissions)
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
@@ -147,7 +147,7 @@ class Customer(TransactionBase):
contact = frappe.get_doc('Contact', contact_name.get('name'))
if not contact.has_link('Customer', self.name):
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- contact.save()
+ contact.save(ignore_permissions=self.flags.ignore_permissions)
else:
lead.lead_name = lead.lead_name.lstrip().split(" ")
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index ee6b429ccae..dfb284b7682 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -108,6 +108,10 @@ class TestQuotation(unittest.TestCase):
sales_order.transaction_date = nowdate()
sales_order.insert()
+ # Remove any unknown taxes if applied
+ sales_order.set('taxes', [])
+ sales_order.save()
+
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index b4e151b2e30..423922e4865 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -169,7 +169,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
}
// project
- if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) {
+ if(flt(doc.per_delivered, 2) < 100) {
this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create'));
}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index ae0faf2128b..fcde0d502e9 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -88,6 +88,8 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(len(si.get("items")), 1)
si.insert()
+ si.set('taxes', [])
+ si.save()
self.assertEqual(si.payment_schedule[0].payment_amount, 500.0)
self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date)
@@ -401,6 +403,22 @@ class TestSalesOrder(unittest.TestCase):
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
+ def test_update_child_with_precision(self):
+ from frappe.model.meta import get_field_precision
+ from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+
+ precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate"))
+
+ make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency")
+ so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664)
+
+ trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}])
+ update_child_qty_rate('Sales Order', trans_item, so.name)
+
+ so.reload()
+ self.assertEqual(so.items[0].rate, 200.34669)
+ make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
+
def test_update_child_qty_rate_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
@@ -425,9 +443,9 @@ class TestSalesOrder(unittest.TestCase):
frappe.set_user("Administrator")
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
+ frappe.set_user("Administrator")
apply_workflow(so, 'Approve')
- frappe.set_user("Administrator")
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver")
@@ -474,6 +492,95 @@ class TestSalesOrder(unittest.TestCase):
so.reload()
self.assertEqual(so.packed_items[0].qty, 4)
+ def test_update_child_with_tax_template(self):
+ """
+ Test Action: Create a SO with one item having its tax account head already in the SO.
+ Add the same item + new item with tax template via Update Items.
+ Expected result: First Item's tax row is updated. New tax row is added for second Item.
+ """
+ if not frappe.db.exists("Item", "Test Item with Tax"):
+ make_item("Test Item with Tax", {
+ 'is_stock_item': 1,
+ })
+
+ if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
+ frappe.get_doc({
+ 'doctype': 'Item Tax Template',
+ 'title': 'Test Update Items Template',
+ 'company': '_Test Company',
+ 'taxes': [
+ {
+ 'tax_type': "_Test Account Service Tax - _TC",
+ 'tax_rate': 10,
+ }
+ ]
+ }).insert()
+
+ new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
+
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
+
+ tax_template = "_Test Account Excise Duty @ 10"
+ item = "_Test Item Home Desktop 100"
+ if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
+ item_doc = frappe.get_doc("Item", item)
+ item_doc.append("taxes", {
+ "item_tax_template": tax_template,
+ "valid_from": nowdate()
+ })
+ item_doc.save()
+ else:
+ # update valid from
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
+ where parent = %(item)s and item_tax_template = %(tax)s""",
+ {"item": item, "tax": tax_template})
+
+ so = make_sales_order(item_code=item, qty=1, do_not_save=1)
+
+ so.append("taxes", {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10
+ })
+ so.insert()
+ so.submit()
+
+ self.assertEqual(so.taxes[0].tax_amount, 10)
+ self.assertEqual(so.taxes[0].total, 110)
+
+ old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse")
+ frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC")
+
+ items = json.dumps([
+ {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name},
+ {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO
+ {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
+ ])
+ update_child_qty_rate('Sales Order', items, so.name)
+
+ so.reload()
+ self.assertEqual(so.taxes[0].tax_amount, 40)
+ self.assertEqual(so.taxes[0].total, 440)
+ self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC")
+ self.assertEqual(so.taxes[1].tax_amount, 40)
+ self.assertEqual(so.taxes[1].total, 480)
+
+ # teardown
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
+ where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
+ so.cancel()
+ so.delete()
+ new_item_with_tax.delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
+
def test_warehouse_user(self):
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
@@ -1010,7 +1117,7 @@ def make_sales_order_workflow():
"is_active": 1,
"send_email_alert": 0,
})
- workflow.append('states', dict( state = 'Pending', allow_edit = 'All' ))
+ workflow.append('states', dict( state = 'Pending', allow_edit = 'Administrator' ))
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
workflow.append('transitions', dict(
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 335cad3598b..60dacb54d1a 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -372,7 +372,7 @@ class Company(NestedSet):
@frappe.whitelist()
def enqueue_replace_abbr(company, old, new):
- kwargs = dict(company=company, old=old, new=new)
+ kwargs = dict(queue='long', company=company, old=old, new=new)
frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)
diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
index b2cffbbf0d8..abff97364c0 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ b/erpnext/setup/doctype/naming_series/naming_series.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cstr
+from frappe.utils import cstr, cint
from frappe import msgprint, throw, _
from frappe.model.document import Document
@@ -159,7 +159,7 @@ class NamingSeries(Document):
prefix = self.parse_naming_series()
self.insert_series(prefix)
frappe.db.sql("update `tabSeries` set current = %s where name = %s",
- (self.current_value, prefix))
+ (cint(self.current_value), prefix))
msgprint(_("Series Updated Successfully"))
else:
msgprint(_("Please select prefix first"))
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index deace33f343..e6634d29fe1 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -111,6 +111,7 @@ class Item(WebsiteGenerator):
self.synced_with_hub = 0
self.validate_has_variants()
+ self.validate_attributes_in_variants()
self.validate_stock_exists_for_template_item()
self.validate_attributes()
self.validate_variant_attributes()
@@ -806,6 +807,76 @@ class Item(WebsiteGenerator):
if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants."))
+ def validate_attributes_in_variants(self):
+ if not self.has_variants or self.get("__islocal"):
+ return
+
+ old_doc = self.get_doc_before_save()
+ old_doc_attributes = set([attr.attribute for attr in old_doc.attributes])
+ own_attributes = [attr.attribute for attr in self.attributes]
+
+ # Check if old attributes were removed from the list
+ # Is old_attrs is a subset of new ones
+ # that means we need not check any changes
+ if old_doc_attributes.issubset(set(own_attributes)):
+ return
+
+ from collections import defaultdict
+
+ # get all item variants
+ items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})]
+
+ # get all deleted attributes
+ deleted_attribute = list(old_doc_attributes.difference(set(own_attributes)))
+
+ # fetch all attributes of these items
+ item_attributes = frappe.get_all(
+ "Item Variant Attribute",
+ filters={
+ "parent": ["in", items],
+ "attribute": ["in", deleted_attribute]
+ },
+ fields=["attribute", "parent"]
+ )
+ not_included = defaultdict(list)
+
+ for attr in item_attributes:
+ if attr["attribute"] not in own_attributes:
+ not_included[attr["parent"]].append(attr["attribute"])
+
+ if not len(not_included):
+ return
+
+ def body(docnames):
+ docnames.sort()
+ return "
".join(docnames)
+
+ def table_row(title, body):
+ return """
+ | {0} |
+ {1} |
+
""".format(title, body)
+
+ rows = ''
+ for docname, attr_list in not_included.items():
+ link = "{0}".format(frappe.bold(_(docname)))
+ rows += table_row(link, body(attr_list))
+
+ error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
+
+ message = """
+ {0}
+
+ """.format(error_description, _('Variant Items'), _('Attributes'), rows)
+
+ frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True)
+
def validate_stock_exists_for_template_item(self):
if self.stock_ledger_created() and self._doc_before_save:
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)
@@ -1001,8 +1072,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
order by pr.posting_date desc, pr.posting_time desc, pr.name desc
limit 1""", (item_code, cstr(doc_name)), as_dict=1)
-
-
+
+
purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date
or "1900-01-01")
purchase_receipt_date = getdate(last_purchase_receipt and
@@ -1010,7 +1081,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt):
# use purchase order
-
+
last_purchase = last_purchase_order[0]
purchase_date = purchase_order_date
@@ -1030,7 +1101,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"discount_percentage": flt(last_purchase.discount_percentage),
"purchase_date": purchase_date
})
-
+
conversion_rate = flt(conversion_rate) or 1.0
out.update({
@@ -1069,8 +1140,7 @@ def invalidate_item_variants_cache_for_website(doc):
if item_code:
item_cache = ItemVariantsCacheManager(item_code)
- item_cache.clear_cache()
-
+ item_cache.rebuild_cache()
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.json b/erpnext/stock/doctype/item_attribute/item_attribute.json
index 2fbff4e614e..5c4678916f3 100644
--- a/erpnext/stock/doctype/item_attribute/item_attribute.json
+++ b/erpnext/stock/doctype/item_attribute/item_attribute.json
@@ -1,357 +1,97 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:attribute_name",
- "beta": 0,
- "creation": "2014-09-26 03:49:54.899170",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:attribute_name",
+ "creation": "2014-09-26 03:49:54.899170",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "attribute_name",
+ "numeric_values",
+ "section_break_4",
+ "from_range",
+ "increment",
+ "column_break_8",
+ "to_range",
+ "section_break_5",
+ "item_attribute_values"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "attribute_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Attribute Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "fieldname": "attribute_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Attribute Name",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "fieldname": "numeric_values",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Numeric Values",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "numeric_values",
+ "fieldtype": "Check",
+ "label": "Numeric Values"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "numeric_values",
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "numeric_values",
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "from_range",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From Range",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "from_range",
+ "fieldtype": "Float",
+ "label": "From Range"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "increment",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Increment",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "increment",
+ "fieldtype": "Float",
+ "label": "Increment"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_8",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "to_range",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To Range",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "to_range",
+ "fieldtype": "Float",
+ "label": "To Range"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval: !doc.numeric_values",
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval: !doc.numeric_values",
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "item_attribute_values",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Attribute Values",
- "length": 0,
- "no_copy": 0,
- "options": "Item Attribute Value",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "item_attribute_values",
+ "fieldtype": "Table",
+ "label": "Item Attribute Values",
+ "options": "Item Attribute Value"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-edit",
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-01 13:17:46.524806",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Item Attribute",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-edit",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-10-02 12:03:02.359202",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Attribute",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 1,
- "role": "Item Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Item Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py
index 2f75bbd97c0..7f00201587a 100644
--- a/erpnext/stock/doctype/item_attribute/item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/item_attribute.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
+from frappe.utils import flt
from erpnext.controllers.item_variant import (validate_is_incremental,
validate_item_attribute_value, InvalidItemAttributeValueError)
@@ -42,7 +43,7 @@ class ItemAttribute(Document):
if self.from_range is None or self.to_range is None:
frappe.throw(_("Please specify from/to range"))
- elif self.from_range >= self.to_range:
+ elif flt(self.from_range) >= flt(self.to_range):
frappe.throw(_("From Range has to be less than To Range"))
if not self.increment:
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/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 42cc473b18d..8538b00fd6e 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -207,7 +207,7 @@ class PurchaseReceipt(BuyingController):
from erpnext.accounts.general_ledger import process_gl_map
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
- cogs_account = self.get_company_default("default_expense_account")
+ stock_rbnb_currency = get_account_currency(stock_rbnb)
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -243,7 +243,6 @@ class PurchaseReceipt(BuyingController):
# stock received but not billed
if d.base_net_amount:
- stock_rbnb_currency = get_account_currency(stock_rbnb)
gl_entries.append(self.get_gl_dict({
"account": stock_rbnb,
"against": warehouse_account[d.warehouse]["account"],
@@ -289,6 +288,7 @@ class PurchaseReceipt(BuyingController):
if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation
else:
+ cogs_account = self.get_company_default("default_expense_account")
loss_account = cogs_account
gl_entries.append(self.get_gl_dict({
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index c9cda37c40b..d0208d01eda 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -471,8 +471,7 @@ class TestPurchaseReceipt(unittest.TestCase):
"expected_value_after_useful_life": 10,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 1,
- "depreciation_start_date": frappe.utils.nowdate()
+ "frequency_of_depreciation": 1
})
asset.submit()
@@ -614,9 +613,9 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
+ "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name},
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}
+ "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}
]
rm_item_string = json.dumps(rm_items)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 2d32999c5c5..ef1cbda3fe5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -24,6 +24,24 @@ frappe.ui.form.on('Stock Entry', {
}
});
+ frm.set_query('source_warehouse_address', function() {
+ return {
+ filters: {
+ link_doctype: 'Warehouse',
+ link_name: frm.doc.from_warehouse
+ }
+ }
+ });
+
+ frm.set_query('target_warehouse_address', function() {
+ return {
+ filters: {
+ link_doctype: 'Warehouse',
+ link_name: frm.doc.to_warehouse
+ }
+ }
+ });
+
frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => {
if (r.sample_retention_warehouse) {
var filters = [
@@ -139,6 +157,7 @@ frappe.ui.form.on('Stock Entry', {
mr_item.item_code = item.item_code;
mr_item.item_name = item.item_name;
mr_item.uom = item.uom;
+ mr_item.stock_uom = item.stock_uom;
mr_item.conversion_factor = item.conversion_factor;
mr_item.item_group = item.item_group;
mr_item.description = item.description;
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 86e4b3f8b3d..b61b8e37197 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -844,6 +844,8 @@ class StockEntry(StockController):
frappe.throw(_("Posting date and posting time is mandatory"))
self.set_work_order_details()
+ self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
+ "backflush_raw_materials_based_on")
if self.bom_no:
@@ -857,14 +859,14 @@ class StockEntry(StockController):
item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict)
- elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
- and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings",
- "backflush_raw_materials_based_on")== "Material Transferred for Manufacture"):
+ elif (self.work_order and (self.purpose == "Manufacture"
+ or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer
+ and self.flags.backflush_based_on == "Material Transferred for Manufacture"):
self.get_transfered_raw_materials()
- elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \
- frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \
- frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1:
+ elif (self.work_order and (self.purpose == "Manufacture" or
+ self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM"
+ and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
self.get_unconsumed_raw_materials()
else:
if not self.fg_completed_qty:
@@ -1108,7 +1110,6 @@ class StockEntry(StockController):
for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse):
if (qty > req_qty):
- qty = req_qty
qty-= d.get(item.warehouse)
if qty > 0:
@@ -1133,11 +1134,22 @@ class StockEntry(StockController):
"""
item_dict = self.get_pro_order_required_items()
max_qty = flt(self.pro_doc.qty)
+
+ allow_overproduction = False
+ overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
+ "overproduction_percentage_for_work_order"))
+
+ to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty)
+ transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100)
+
+ if transfer_limit_qty >= to_transfer_qty:
+ allow_overproduction = True
+
for item, item_details in iteritems(item_dict):
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
- if desire_to_transfer <= pending_to_issue:
+ if desire_to_transfer <= pending_to_issue or allow_overproduction:
item_dict[item]["qty"] = desire_to_transfer
elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue
@@ -1255,8 +1267,9 @@ class StockEntry(StockController):
FROM
`tabStock Entry Detail` sed, `tabStock Entry` se
WHERE
- (pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code)
- AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s
+ pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
+ AND pos.parent = se.purchase_order AND sed.docstatus = 1
+ AND se.name = sed.parent and se.purchase_order = %(po)s
), 0)
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 0475ea7a2ec..0b6610cf85b 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -109,6 +109,10 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
+
+ if (frm.doc.purpose == "Stock Reconciliation" && !d.serial_no) {
+ frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
+ }
}
});
}
@@ -183,6 +187,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
frappe.model.set_value(cdt, cdn, "batch_no", "");
}
+ if (child.serial_no) {
+ frappe.model.set_value(cdt, cdn, "serial_no", "");
+ frappe.model.set_value(cdt, cdn, "current_serial_no", "");
+ }
+
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
},
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index ca59e67a676..2b4780437f4 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -68,6 +68,8 @@ class StockReconciliation(StockController):
if item_dict.get("serial_nos"):
item.current_serial_no = item_dict.get("serial_nos")
+ if self.purpose == "Stock Reconciliation" and not item.serial_no:
+ item.serial_no = item.current_serial_no
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
@@ -172,8 +174,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 +194,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 +221,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 +253,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..27908016407 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -131,7 +131,7 @@ class TestStockReconciliation(unittest.TestCase):
to_delete_records.append(sr.name)
sr = create_stock_reconciliation(item_code=serial_item_code,
- warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos))
+ warehouse = serial_warehouse, qty=5, rate=300)
# print(sr.name)
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
@@ -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/get_item_details.py b/erpnext/stock/get_item_details.py
index 50c8c4e53f4..b1e38b340c5 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -385,6 +385,11 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
else:
warehouse = args.get('warehouse')
+ if not warehouse:
+ default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
+ if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company:
+ return default_warehouse
+
return warehouse
def update_barcode_value(out):
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 b23c908e07a..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,19 +3,70 @@
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"),
"fieldtype": "Date",
"width": "80",
"default": frappe.sys_defaults.year_start_date,
+ "reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"width": "80",
- "default": frappe.datetime.get_today()
- }
+ "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 7f7835f74ee..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
@@ -9,6 +9,9 @@ from frappe.utils import flt, cint, getdate
def execute(filters=None):
if not filters: filters = {}
+ if filters.from_date > filters.to_date:
+ frappe.throw(_("From Date must be before To Date"))
+
float_precision = cint(frappe.db.get_default("float_precision")) or 3
columns = get_columns(filters)
@@ -50,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_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 99d816c4a24..953939bccb8 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -17,14 +17,17 @@ def execute(filters=None):
data = []
for item, item_dict in iteritems(item_details):
+ earliest_age, latest_age = 0, 0
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
details = item_dict["details"]
- if not fifo_queue or (not item_dict.get("total_qty")): continue
+ if not fifo_queue and (not item_dict.get("total_qty")): continue
average_age = get_average_age(fifo_queue, to_date)
- earliest_age = date_diff(to_date, fifo_queue[0][1])
- latest_age = date_diff(to_date, fifo_queue[-1][1])
+
+ if fifo_queue:
+ earliest_age = date_diff(to_date, fifo_queue[0][1])
+ latest_age = date_diff(to_date, fifo_queue[-1][1])
row = [details.name, details.item_name,
details.description, details.item_group, details.brand]
@@ -147,7 +150,8 @@ def get_fifo_queue(filters, sle=None):
item_details.setdefault(key, {"details": d, "fifo_queue": []})
fifo_queue = item_details[key]["fifo_queue"]
- transferred_item_details.setdefault((d.voucher_no, d.name), [])
+ transferred_item_key = (d.voucher_no, d.name, d.warehouse)
+ transferred_item_details.setdefault(transferred_item_key, [])
if d.voucher_type == "Stock Reconciliation":
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
@@ -155,10 +159,10 @@ def get_fifo_queue(filters, sle=None):
serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
if d.actual_qty > 0:
- if transferred_item_details.get((d.voucher_no, d.name)):
- batch = transferred_item_details[(d.voucher_no, d.name)][0]
+ if transferred_item_details.get(transferred_item_key):
+ batch = transferred_item_details[transferred_item_key][0]
fifo_queue.append(batch)
- transferred_item_details[((d.voucher_no, d.name))].pop(0)
+ transferred_item_details[transferred_item_key].pop(0)
else:
if serial_no_list:
for serial_no in serial_no_list:
@@ -182,11 +186,11 @@ def get_fifo_queue(filters, sle=None):
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
qty_to_pop -= flt(batch[0])
- transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0))
+ transferred_item_details[transferred_item_key].append(fifo_queue.pop(0))
else:
# all from current batch
batch[0] = flt(batch[0]) - qty_to_pop
- transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]])
+ transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]])
qty_to_pop = 0
item_details[key]["qty_after_transaction"] = d.qty_after_transaction
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:
diff --git a/requirements.txt b/requirements.txt
index c277545fab5..f807fa6c29d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
pandas==0.24.2
-plaid-python==3.4.0
+plaid-python==6.0.0
PyGithub==1.44.1
python-stdnum==1.12
Unidecode==1.1.1