Redeem Loyalty Points
${amount}
${loyalty_program}
@@ -563,4 +563,4 @@ erpnext.PointOfSale.Payment = class {
toggle_component(show) {
show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
-};
\ No newline at end of file
+};
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index f5feb95f1a8..8cb24460f7b 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -59,7 +59,7 @@ def get_data(conditions, filters):
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty,
- IFNULL(sii.qty, 0) as billed_qty,
+ IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 9dc7fc42ef0..d05541b6344 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -90,12 +90,6 @@ frappe.ui.form.on("Company", {
frm.toggle_enable("default_currency", (frm.doc.__onload &&
!frm.doc.__onload.transactions_exist));
- if (frm.has_perm('write')) {
- frm.add_custom_button(__('Create Tax Template'), function() {
- frm.trigger("make_default_tax_template");
- });
- }
-
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
frm.add_custom_button(__('Cost Centers'), function() {
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
@@ -121,17 +115,21 @@ frappe.ui.form.on("Company", {
}
if (frm.has_perm('write')) {
- frm.add_custom_button(__('Default Tax Template'), function() {
+ frm.add_custom_button(__('Create Tax Template'), function() {
frm.trigger("make_default_tax_template");
- }, __('Create'));
+ }, __('Manage'));
+ }
+
+ if (frappe.user.has_role('System Manager')) {
+ if (frm.has_perm('write')) {
+ frm.add_custom_button(__('Delete Transactions'), function() {
+ frm.trigger("delete_company_transactions");
+ }, __('Manage'));
+ }
}
}
erpnext.company.set_chart_of_accounts_options(frm.doc);
-
- if (!frappe.user.has_role('System Manager')) {
- frm.get_field("delete_company_transactions").hide();
- }
},
make_default_tax_template: function(frm) {
@@ -145,11 +143,6 @@ frappe.ui.form.on("Company", {
})
},
- onload_post_render: function(frm) {
- if(frm.get_field("delete_company_transactions").$input)
- frm.get_field("delete_company_transactions").$input.addClass("btn-danger");
- },
-
country: function(frm) {
erpnext.company.set_chart_of_accounts_options(frm.doc);
},
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 83cbf475abd..061986d92d7 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -99,7 +99,6 @@
"company_description",
"registration_info",
"registration_details",
- "delete_company_transactions",
"lft",
"rgt",
"old_parent"
@@ -666,11 +665,6 @@
"oldfieldname": "registration_details",
"oldfieldtype": "Code"
},
- {
- "fieldname": "delete_company_transactions",
- "fieldtype": "Button",
- "label": "Delete Company Transactions"
- },
{
"fieldname": "lft",
"fieldtype": "Int",
@@ -747,7 +741,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-02-16 15:53:37.167589",
+ "modified": "2021-05-07 03:11:28.189740",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 077538d479c..0427abe558d 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -54,7 +54,7 @@ class Company(NestedSet):
def validate_abbr(self):
if not self.abbr:
- self.abbr = ''.join([c[0] for c in self.company_name.split()]).upper()
+ self.abbr = ''.join(c[0] for c in self.company_name.split()).upper()
self.abbr = self.abbr.strip()
@@ -335,7 +335,7 @@ class Company(NestedSet):
clear_defaults_cache()
def abbreviate(self):
- self.abbr = ''.join([c[0].upper() for c in self.company_name.split()])
+ self.abbr = ''.join(c[0].upper() for c in self.company_name.split())
def on_trash(self):
"""
@@ -407,8 +407,6 @@ def replace_abbr(company, old, new):
frappe.only_for("System Manager")
- frappe.db.set_value("Company", company, "abbr", new)
-
def _rename_record(doc):
parts = doc[0].rsplit(" - ", 1)
if len(parts) == 1 or parts[1].lower() == old.lower():
@@ -419,11 +417,18 @@ def replace_abbr(company, old, new):
doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company))
for d in doc:
_rename_record(d)
+ try:
+ frappe.db.auto_commit_on_many_writes = 1
+ frappe.db.set_value("Company", company, "abbr", new)
+ for dt in ["Warehouse", "Account", "Cost Center", "Department",
+ "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
+ _rename_records(dt)
+ frappe.db.commit()
- for dt in ["Warehouse", "Account", "Cost Center", "Department",
- "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
- _rename_records(dt)
- frappe.db.commit()
+ except Exception:
+ frappe.log_error(title=_('Abbreviation Rename Error'))
+ finally:
+ frappe.db.auto_commit_on_many_writes = 0
def get_name_with_abbr(name, company):
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index 8c97322a71a..340d89bdf8e 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -249,7 +249,7 @@ class EmailDigest(Document):
card = cache.get(cache_key)
if card:
- card = eval(card)
+ card = frappe.safe_eval(card)
else:
card = frappe._dict(getattr(self, "get_" + key)())
@@ -808,7 +808,6 @@ def get_incomes_expenses_for_period(account, from_date, to_date):
val = balance_on_to_date - balance_before_from_date
else:
last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1))
- print(fy_start_date - timedelta(days=1), last_year_closing_balance)
val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date)
return val
@@ -837,4 +836,4 @@ def get_future_date_for_calendaer_event(frequency):
elif frequency == "Monthly":
to_date = add_to_date(from_date, months=1)
- return from_date, to_date
\ No newline at end of file
+ return from_date, to_date
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index e5872171815..a0ba1efb5b4 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -59,13 +59,15 @@ class GlobalDefaults(Document):
# Make property setters to hide rounded total fields
for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note",
- "Supplier Quotation", "Purchase Order", "Purchase Invoice"):
+ "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"):
make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
+ make_property_setter(doctype, "disable_rounded_total", "default", cint(self.disable_rounded_total), "Text", validate_fields_for_doctype=False)
+
def toggle_in_words(self):
self.disable_in_words = cint(self.disable_in_words)
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index bff806d5472..1c72cebfa9d 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
field_filters['item_group'] = self.name
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start)
+ context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
filter_engine = ProductFiltersBuilder(self.name)
@@ -139,7 +139,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non
# return child item groups if the type is of "Is Group"
return get_child_groups_for_list_in_html(item_group, start, limit, search)
- child_groups = ", ".join([frappe.db.escape(i[0]) for i in get_child_groups(product_group)])
+ child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group))
# base query
query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group,
@@ -239,7 +239,7 @@ def get_item_for_list_in_html(context):
return frappe.get_template(products_template).render(context)
def get_group_item_count(item_group):
- child_groups = ", ".join(['"' + i[0] + '"' for i in get_child_groups(item_group)])
+ child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group))
return frappe.db.sql("""select count(*) from `tabItem`
where docstatus = 0 and show_in_website = 1
and (item_group in (%s)
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index 38f8de7a660..ece9fb56992 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -12,10 +12,6 @@ from frappe.desk.notifications import clear_notifications
class TransactionDeletionRecord(Document):
def validate(self):
frappe.only_for('System Manager')
- company_obj = frappe.get_doc('Company', self.company)
- if frappe.session.user != company_obj.owner and frappe.session.user != 'Administrator':
- frappe.throw(_('Transactions can only be deleted by the creator of the Company or the Administrator.'),
- frappe.PermissionError)
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list:
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index c7220cbc071..bbee74cafb4 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -39,7 +39,7 @@ def check_setup_wizard_not_completed():
if cint(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed.
You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall"""
- frappe.throw(message)
+ frappe.throw(message) # nosemgrep
def set_single_defaults():
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 58764880330..daaa626a81a 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -481,37 +481,42 @@
},
"Germany": {
+ "tax_categories": [
+ "Umsatzsteuer",
+ "Vorsteuer"
+ ],
"chart_of_accounts": {
"SKR04 mit Kontonummern": {
"sales_tax_templates": [
{
- "title": "Umsatzsteuer 19%",
+ "title": "Umsatzsteuer",
+ "tax_category": "Umsatzsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
"account_name": "Umsatzsteuer 19%",
"account_number": "3806",
"tax_rate": 19.00
- }
- }
- ]
- },
- {
- "title": "Umsatzsteuer 7%",
- "taxes": [
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Umsatzsteuer 7%",
"account_number": "3801",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
}
]
}
],
"purchase_tax_templates": [
{
- "title": "Abziehbare Vorsteuer 19%",
+ "title": "Vorsteuer",
+ "tax_category": "Vorsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
@@ -519,20 +524,17 @@
"account_number": "1406",
"root_type": "Asset",
"tax_rate": 19.00
- }
- }
- ]
- },
- {
- "title": "Abziehbare Vorsteuer 7%",
- "taxes": [
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1401",
"root_type": "Asset",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
}
]
},
@@ -559,38 +561,129 @@
}
]
}
- ]
- },
- "SKR03 mit Kontonummern": {
- "sales_tax_templates": [
+ ],
+ "item_tax_templates": [
{
"title": "Umsatzsteuer 19%",
"taxes": [
{
- "account_head": {
+ "tax_type": {
"account_name": "Umsatzsteuer 19%",
- "account_number": "1776",
+ "account_number": "3806",
"tax_rate": 19.00
- }
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "3801",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
}
]
},
{
"title": "Umsatzsteuer 7%",
"taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "3806",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "3801",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1406",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1401",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1406",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1401",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
+ }
+ ]
+ }
+ ]
+ },
+ "SKR03 mit Kontonummern": {
+ "sales_tax_templates": [
+ {
+ "title": "Umsatzsteuer",
+ "tax_category": "Umsatzsteuer",
+ "is_default": 1,
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "1776",
+ "tax_rate": 19.00
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Umsatzsteuer 7%",
"account_number": "1771",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
}
]
}
],
"purchase_tax_templates": [
{
- "title": "Abziehbare Vorsteuer 19%",
+ "title": "Vorsteuer",
+ "tax_category": "Vorsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
@@ -598,20 +691,107 @@
"account_number": "1576",
"root_type": "Asset",
"tax_rate": 19.00
- }
- }
- ]
- },
- {
- "title": "Abziehbare Vorsteuer 7%",
- "taxes": [
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1571",
"root_type": "Asset",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
+ }
+ ]
+ }
+ ],
+ "item_tax_templates": [
+ {
+ "title": "Umsatzsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "1776",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "1771",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Umsatzsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "1776",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "1771",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1576",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1571",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1576",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1571",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
}
]
}
@@ -620,33 +800,34 @@
"Standard with Numbers": {
"sales_tax_templates": [
{
- "title": "Umsatzsteuer 19%",
+ "title": "Umsatzsteuer",
+ "tax_category": "Umsatzsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
"account_name": "Umsatzsteuer 19%",
"account_number": "2301",
"tax_rate": 19.00
- }
- }
- ]
- },
- {
- "title": "Umsatzsteuer 7%",
- "taxes": [
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Umsatzsteuer 7%",
"account_number": "2302",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
}
]
}
],
"purchase_tax_templates": [
{
- "title": "Abziehbare Vorsteuer 19%",
+ "title": "Vorsteuer",
+ "tax_category": "Vorsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
@@ -654,20 +835,107 @@
"account_number": "1501",
"root_type": "Asset",
"tax_rate": 19.00
- }
- }
- ]
- },
- {
- "title": "Abziehbare Vorsteuer 7%",
- "taxes": [
+ },
+ "rate": 0.00
+ },
{
"account_head": {
"account_name": "Abziehbare Vorsteuer 7%",
"account_number": "1502",
"root_type": "Asset",
"tax_rate": 7.00
- }
+ },
+ "rate": 0.00
+ }
+ ]
+ }
+ ],
+ "item_tax_templates": [
+ {
+ "title": "Umsatzsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "2301",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "2302",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Umsatzsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "account_number": "2301",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "account_number": "2302",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1501",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1502",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "account_number": "1501",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "account_number": "1502",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
}
]
}
@@ -676,13 +944,69 @@
"*": {
"sales_tax_templates": [
{
- "title": "Umsatzsteuer 19%",
+ "title": "Umsatzsteuer",
+ "tax_category": "Umsatzsteuer",
+ "is_default": 1,
"taxes": [
{
"account_head": {
"account_name": "Umsatzsteuer 19%",
"tax_rate": 19.00
- }
+ },
+ "rate": 0.00
+ },
+ {
+ "account_head": {
+ "account_name": "Umsatzsteuer 7%",
+ "tax_rate": 7.00
+ },
+ "rate": 0.00
+ }
+ ]
+ }
+ ],
+ "purchase_tax_templates": [
+ {
+ "title": "Vorsteuer 19%",
+ "tax_category": "Vorsteuer",
+ "is_default": 1,
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "tax_rate": 19.00,
+ "root_type": "Asset"
+ },
+ "rate": 0.00
+ },
+ {
+ "account_head": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "rate": 0.00
+ }
+ ]
+ }
+ ],
+ "item_tax_templates": [
+ {
+ "title": "Umsatzsteuer 19%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 7%",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 0.00
}
]
},
@@ -690,36 +1014,60 @@
"title": "Umsatzsteuer 7%",
"taxes": [
{
- "account_head": {
+ "tax_type": {
+ "account_name": "Umsatzsteuer 19%",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
"account_name": "Umsatzsteuer 7%",
"tax_rate": 7.00
- }
- }
- ]
- }
- ],
- "purchase_tax_templates": [
- {
- "title": "Abziehbare Vorsteuer 19%",
- "taxes": [
- {
- "account_head": {
- "account_name": "Abziehbare Vorsteuer 19%",
- "tax_rate": 19.00,
- "root_type": "Asset"
- }
+ },
+ "tax_rate": 7.00
}
]
},
{
- "title": "Abziehbare Vorsteuer 7%",
+ "title": "Vorsteuer 19%",
"taxes": [
{
- "account_head": {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 19.00
+ },
+ {
+ "tax_type": {
"account_name": "Abziehbare Vorsteuer 7%",
"root_type": "Asset",
"tax_rate": 7.00
- }
+ },
+ "tax_rate": 0.00
+ }
+ ]
+ },
+ {
+ "title": "Vorsteuer 7%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 19%",
+ "root_type": "Asset",
+ "tax_rate": 19.00
+ },
+ "tax_rate": 0.00
+ },
+ {
+ "tax_type": {
+ "account_name": "Abziehbare Vorsteuer 7%",
+ "root_type": "Asset",
+ "tax_rate": 7.00
+ },
+ "tax_rate": 7.00
}
]
}
@@ -1519,7 +1867,7 @@
"South Africa": {
"South Africa Tax": {
"account_name": "VAT",
- "tax_rate": 14.00
+ "tax_rate": 15.00
}
},
diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index 429a558c589..f4fe18e116c 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -11,6 +11,9 @@ from frappe import _
def setup_taxes_and_charges(company_name: str, country: str):
+ if not frappe.db.exists('Company', company_name):
+ frappe.throw(_('Company {} does not exist yet. Taxes setup aborted.').format(company_name))
+
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json')
with open(file_path, 'r') as json_file:
tax_data = json.load(json_file)
@@ -23,7 +26,7 @@ def setup_taxes_and_charges(company_name: str, country: str):
if 'chart_of_accounts' not in country_wise_tax:
country_wise_tax = simple_to_detailed(country_wise_tax)
- from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts'))
+ from_detailed_data(company_name, country_wise_tax)
def simple_to_detailed(templates):
@@ -74,10 +77,16 @@ def simple_to_detailed(templates):
def from_detailed_data(company_name, data):
"""Create Taxes and Charges Templates from detailed data."""
coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts')
- tax_templates = data.get(coa_name) or data.get('*')
- sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*')
- purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*')
- item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*')
+ coa_data = data.get('chart_of_accounts', {})
+ tax_templates = coa_data.get(coa_name) or coa_data.get('*', {})
+ tax_categories = data.get('tax_categories')
+ sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {})
+ purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {})
+ item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {})
+
+ if tax_categories:
+ for tax_category in tax_categories:
+ make_tax_catgory(tax_category)
if sales_tax_templates:
for template in sales_tax_templates:
@@ -106,6 +115,9 @@ def make_taxes_and_charges_template(company_name, doctype, template):
'charge_type': 'On Net Total'
}
+ if doctype == 'Purchase Taxes and Charges Template':
+ tax_row_defaults['add_deduct_tax'] = 'Add'
+
# if account_head is a dict, search or create the account and get it's name
if isinstance(account_data, dict):
tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate'))
@@ -230,3 +242,14 @@ def get_or_create_tax_group(company_name, root_type):
tax_group_name = tax_group_account.name
return tax_group_name
+
+
+def make_tax_catgory(tax_category):
+ doctype = 'Tax Category'
+ if isinstance(tax_category, str):
+ tax_category = {'title': tax_category}
+
+ tax_category['doctype'] = doctype
+ if not frappe.db.exists(doctype, tax_category['title']):
+ doc = frappe.get_doc(tax_category)
+ doc.insert(ignore_permissions=True)
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py
index 6c63d8759b4..7dfa09e2d62 100644
--- a/erpnext/shopping_cart/filters.py
+++ b/erpnext/shopping_cart/filters.py
@@ -22,12 +22,15 @@ class ProductFiltersBuilder:
filter_data = []
for df in fields:
- filters = {}
+ filters, or_filters = {}, []
if df.fieldtype == "Link":
if self.item_group:
- filters['item_group'] = self.item_group
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+ values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
else:
doctype = df.get_link_doctype()
@@ -44,7 +47,9 @@ class ProductFiltersBuilder:
values = [d.name for d in frappe.get_all(doctype, filters)]
# Remove None
- values = values.remove(None) if None in values else values
+ if None in values:
+ values.remove(None)
+
if values:
filter_data.append([df, values])
@@ -61,14 +66,18 @@ class ProductFiltersBuilder:
for attr_doc in attribute_docs:
selected_attributes = []
for attr in attr_doc.item_attribute_values:
+ or_filters = []
filters= [
["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
]
if self.item_group:
- filters.append(["item_group", "=", self.item_group])
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- if frappe.db.get_all("Item", filters, limit=1):
+ if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
if selected_attributes:
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 36d446ed0fd..d96d803416c 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -22,13 +22,14 @@ class ProductQuery:
self.settings = frappe.get_doc("Products Settings")
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20
- self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
+ self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
+ 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
self.filters = []
self.or_filters = [['show_in_website', '=', 1]]
if not self.settings.get('hide_variants'):
self.or_filters.append(['show_variant_in_website', '=', 1])
- def query(self, attributes=None, fields=None, search_term=None, start=0):
+ def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary
Args:
@@ -44,6 +45,15 @@ class ProductQuery:
if search_term: self.build_search_filters(search_term)
result = []
+ website_item_groups = []
+
+ # if from item group page consider website item group table
+ if item_group:
+ website_item_groups = frappe.db.get_all(
+ "Item",
+ fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
+ filters=[["Website Item Group", "item_group", "=", item_group]]
+ )
if attributes:
all_items = []
@@ -65,18 +75,33 @@ class ProductQuery:
)
items_dict = {item.name: item for item in items}
- # TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
else:
- result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length)
+ result = frappe.get_all(
+ "Item",
+ fields=self.fields,
+ filters=self.filters,
+ or_filters=self.or_filters,
+ start=start,
+ limit=self.page_length
+ )
+
+ # Combine results having context of website item groups into item results
+ if item_group and website_item_groups:
+ items_list = {row.name for row in result}
+ for row in website_item_groups:
+ if row.wig_parent not in items_list:
+ result.append(row)
+
+ result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
- item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
+ item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
return result
@@ -90,7 +115,16 @@ class ProductQuery:
if not values:
continue
- if isinstance(values, list):
+ # handle multiselect fields in filter addition
+ meta = frappe.get_meta('Item', cached=True)
+ df = meta.get_field(field)
+ if df.fieldtype == 'Table MultiSelect':
+ child_doctype = df.options
+ child_meta = frappe.get_meta(child_doctype, cached=True)
+ fields = child_meta.get("fields")
+ if fields:
+ self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
+ elif isinstance(values, list):
# If value is a list use `IN` query
self.filters.append([field, 'IN', values])
else:
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py
index d857bf5f5c1..ac61aebc564 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/shopping_cart/test_shopping_cart.py
@@ -7,7 +7,7 @@ import frappe
from frappe.utils import nowdate, add_months
from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party
from erpnext.tests.utils import create_test_contact_and_address
-
+from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
# test_dependencies = ['Payment Terms Template']
@@ -125,7 +125,7 @@ class TestShoppingCart(unittest.TestCase):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
frappe.get_doc(tax_rule).insert()
- except frappe.DuplicateEntryError:
+ except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass
def create_quotation(self):
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 933ca8ab3d4..a657ecf1055 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -268,7 +268,9 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
args: values,
+ btn: dialog.get_primary_btn(),
freeze: true,
+ freeze_message: __('Creating Stock Entry'),
callback: function (r) {
frappe.show_alert(__('Stock Entry {0} created',
['
' + r.message.name + '']));
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index 943cb3401ff..e6d2e1330b5 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "field:batch_id",
"creation": "2013-03-05 14:50:38",
@@ -25,7 +26,11 @@
"reference_doctype",
"reference_name",
"section_break_7",
- "description"
+ "description",
+ "manufacturing_section",
+ "qty_to_produce",
+ "column_break_23",
+ "produced_qty"
],
"fields": [
{
@@ -160,13 +165,35 @@
"label": "Batch UOM",
"options": "UOM",
"read_only": 1
+ },
+ {
+ "fieldname": "manufacturing_section",
+ "fieldtype": "Section Break",
+ "label": "Manufacturing"
+ },
+ {
+ "fieldname": "qty_to_produce",
+ "fieldtype": "Float",
+ "label": "Qty To Produce",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "produced_qty",
+ "fieldtype": "Float",
+ "label": "Produced Qty",
+ "read_only": 1
}
],
"icon": "fa fa-archive",
"idx": 1,
"image_field": "image",
+ "links": [],
"max_attachments": 5,
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-01-07 11:10:09.149170",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 8fdda565d20..cd441b5958d 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -226,9 +226,9 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
return batch.name
-def set_batch_nos(doc, warehouse_field, throw=False):
+def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
"""Automatically select `batch_no` for outgoing items in item table"""
- for d in doc.items:
+ for d in doc.get(child_table):
qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0
has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
warehouse = d.get(warehouse_field, None)
@@ -304,8 +304,13 @@ def validate_serial_no_with_batch(serial_nos, item_code):
frappe.throw(_("The serial no {0} does not belong to item {1}")
.format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code)))
- serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos])
+ serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos)
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
frappe.throw(_("There is no batch found against the {0}: {1}")
- .format(message, serial_no_link))
\ No newline at end of file
+ .format(message, serial_no_link))
+
+def make_batch(args):
+ if frappe.db.get_value("Item", args.item, "has_batch_no"):
+ args.doctype = "Batch"
+ frappe.get_doc(args).insert().name
\ No newline at end of file
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 0514bd23942..43642013ce6 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -54,7 +54,7 @@ class Bin(Document):
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
@@ -115,7 +115,7 @@ class Bin(Document):
#Get Transferred Entries
materials_transferred = frappe.db.sql("""
select
- ifnull(sum(transfer_qty),0)
+ ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
from
`tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
where
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 7875b9cd87f..74cb3fcb1f0 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ frm.set_df_property('packed_items', 'cannot_add_rows', true);
+ frm.set_df_property('packed_items', 'cannot_delete_rows', true);
},
print_without_amount: function(frm) {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 280fde158f5..f20e76f5bf6 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -554,8 +554,7 @@
"oldfieldname": "packing_details",
"oldfieldtype": "Table",
"options": "Packed Item",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "product_bundle_help",
@@ -1289,7 +1288,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-15 23:55:49.620641",
+ "modified": "2021-06-11 19:27:30.901112",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index d326a041730..fcdb5f3b190 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -129,12 +129,13 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc()
- if self._action != 'submit' and not self.is_return:
- set_batch_nos(self, 'warehouse', True)
-
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
+ if self._action != 'submit' and not self.is_return:
+ set_batch_nos(self, 'warehouse', throw=True)
+ set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items")
+
self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed'
@@ -264,7 +265,7 @@ class DeliveryNote(SellingController):
"""
Validate that if packed qty exists, it should be equal to qty
"""
- if not any([flt(d.get('packed_qty')) for d in self.get("items")]):
+ if not any(flt(d.get('packed_qty')) for d in self.get("items")):
return
has_error = False
for d in self.get("items"):
@@ -732,7 +733,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"field_no_map": [
- "taxes_and_charges"
+ "taxes_and_charges",
+ "set_warehouse"
]
},
doctype +" Item": {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index f08125b199d..04028980473 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
- } else if (flt(doc.per_returned, 2) === 100) {
- return [__("Return Issued"), "grey", "per_returned,=,100"];
+ } else if (doc.status === "Return Issued") {
+ return [__("Return Issued"), "grey", "status,=,Return Issued"];
} else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"];
} else if (flt(doc.per_billed, 2) === 100) {
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0c63df0e225..f981aeb13bb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -7,7 +7,7 @@ import unittest
import frappe
import json
import frappe.defaults
-from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
+from frappe.utils import nowdate, nowtime, cstr, flt
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
@@ -18,9 +18,11 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWa
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
import create_stock_reconciliation, set_valuation_method
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
-from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
+from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
-from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self):
@@ -277,8 +279,6 @@ class TestDeliveryNote(unittest.TestCase):
dn.cancel()
def test_sales_return_for_non_bundled_items_full(self):
- from erpnext.stock.doctype.item.test_item import make_item
-
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
make_item("Box", {'is_stock_item': 1})
@@ -741,6 +741,25 @@ class TestDeliveryNote(unittest.TestCase):
self.assertEqual(si2.items[0].qty, 2)
self.assertEqual(si2.items[1].qty, 1)
+
+ def test_delivery_note_bundle_with_batched_item(self):
+ batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0})
+ batched_item = make_item("_Test Batched Item",
+ {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"}
+ )
+ make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
+ make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42)
+
+ try:
+ dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
+ except frappe.ValidationError as e:
+ if "batch" in str(e).lower():
+ self.fail("Batch numbers not getting added to bundled items in DN.")
+ raise e
+
+ self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
+
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 81e730126ec..9ec28d89814 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -68,7 +68,7 @@ class DeliveryTrip(Document):
delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`.
"""
- delivery_notes = list(set([stop.delivery_note for stop in self.delivery_stops if stop.delivery_note]))
+ delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note))
update_fields = {
"driver": self.driver,
@@ -136,8 +136,8 @@ class DeliveryTrip(Document):
# Include last leg in the final distance calculation
self.uom = self.default_distance_uom
- total_distance = sum([leg.get("distance", {}).get("value", 0.0)
- for leg in directions.get("legs")]) # in meters
+ total_distance = sum(leg.get("distance", {}).get("value", 0.0)
+ for leg in directions.get("legs")) # in meters
self.total_distance = total_distance * self.uom_conversion_factor
else:
idx += len(route) - 1
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index dbac79465ee..fbd30cf6c78 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -1,8 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import itertools
import json
import erpnext
@@ -12,7 +10,7 @@ from erpnext.controllers.item_variant import (ItemVariantExistsError,
copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes)
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from frappe import _, msgprint
-from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate,
+from frappe.utils import (cint, cstr, flt, formatdate, getdate,
now_datetime, random_string, strip, get_link_to_form, nowtime)
from frappe.utils.html_utils import clean_html
from frappe.website.doctype.website_slideshow.website_slideshow import \
@@ -21,8 +19,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import \
from frappe.website.render import clear_cache
from frappe.website.website_generator import WebsiteGenerator
-from six import iteritems
-
class DuplicateReorderRows(frappe.ValidationError):
pass
@@ -76,8 +72,6 @@ class Item(WebsiteGenerator):
if not self.description:
self.description = self.item_name
- # if self.is_sales_item and not self.get('is_item_from_hub'):
- # self.publish_in_hub = 1
def after_insert(self):
'''set opening stock and item price'''
@@ -128,8 +122,9 @@ class Item(WebsiteGenerator):
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.update_show_in_website()
+ self.validate_item_tax_net_rate_range()
- if not self.get("__islocal"):
+ if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
self.old_website_item_groups = frappe.db.sql_list("""select item_group
from `tabWebsite Item Group`
@@ -203,7 +198,7 @@ class Item(WebsiteGenerator):
def make_route(self):
if not self.route:
return cstr(frappe.db.get_value('Item Group', self.item_group,
- 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
+ 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
def validate_website_image(self):
if frappe.flags.in_import:
@@ -258,7 +253,6 @@ class Item(WebsiteGenerator):
"attached_to_name": self.name
})
except frappe.DoesNotExistError:
- pass
# cleanup
frappe.local.message_log.pop()
@@ -362,47 +356,49 @@ class Item(WebsiteGenerator):
context.update(get_slideshow(self))
def set_attribute_context(self, context):
- if self.has_variants:
- attribute_values_available = {}
- context.attribute_values = {}
- context.selected_attributes = {}
+ if not self.has_variants:
+ return
- # load attributes
- for v in context.variants:
- v.attributes = frappe.get_all("Item Variant Attribute",
- fields=["attribute", "attribute_value"],
- filters={"parent": v.name})
- # make a map for easier access in templates
- v.attribute_map = frappe._dict({})
- for attr in v.attributes:
- v.attribute_map[attr.attribute] = attr.attribute_value
+ attribute_values_available = {}
+ context.attribute_values = {}
+ context.selected_attributes = {}
- for attr in v.attributes:
- values = attribute_values_available.setdefault(attr.attribute, [])
- if attr.attribute_value not in values:
- values.append(attr.attribute_value)
+ # load attributes
+ for v in context.variants:
+ v.attributes = frappe.get_all("Item Variant Attribute",
+ fields=["attribute", "attribute_value"],
+ filters={"parent": v.name})
+ # make a map for easier access in templates
+ v.attribute_map = frappe._dict({})
+ for attr in v.attributes:
+ v.attribute_map[attr.attribute] = attr.attribute_value
- if v.name == context.variant.name:
- context.selected_attributes[attr.attribute] = attr.attribute_value
+ for attr in v.attributes:
+ values = attribute_values_available.setdefault(attr.attribute, [])
+ if attr.attribute_value not in values:
+ values.append(attr.attribute_value)
- # filter attributes, order based on attribute table
- for attr in self.attributes:
- values = context.attribute_values.setdefault(attr.attribute, [])
+ if v.name == context.variant.name:
+ context.selected_attributes[attr.attribute] = attr.attribute_value
- if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
- for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
- values.append(val)
+ # filter attributes, order based on attribute table
+ for attr in self.attributes:
+ values = context.attribute_values.setdefault(attr.attribute, [])
- else:
- # get list of values defined (for sequence)
- for attr_value in frappe.db.get_all("Item Attribute Value",
- fields=["attribute_value"],
- filters={"parent": attr.attribute}, order_by="idx asc"):
+ if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
+ for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
+ values.append(val)
- if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
- values.append(attr_value.attribute_value)
+ else:
+ # get list of values defined (for sequence)
+ for attr_value in frappe.db.get_all("Item Attribute Value",
+ fields=["attribute_value"],
+ filters={"parent": attr.attribute}, order_by="idx asc"):
- context.variant_info = json.dumps(context.variants)
+ if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
+ values.append(attr_value.attribute_value)
+
+ context.variant_info = json.dumps(context.variants)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
@@ -490,6 +486,11 @@ class Item(WebsiteGenerator):
if self.disabled:
self.show_in_website = False
+ def validate_item_tax_net_rate_range(self):
+ for tax in self.get('taxes'):
+ if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
+ frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
+
def update_template_tables(self):
template = frappe.get_doc("Item", self.variant_of)
@@ -521,7 +522,7 @@ class Item(WebsiteGenerator):
def validate_item_type(self):
if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset:
- msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1)
+ frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item"))
if self.has_serial_no == 0 and self.serial_no_series:
self.serial_no_series = None
@@ -542,10 +543,7 @@ class Item(WebsiteGenerator):
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
- cust_code = []
- for d in self.get('customer_items'):
- cust_code.append(d.ref_code)
- self.customer_code = ','.join(cust_code)
+ self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", []))
def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
@@ -742,23 +740,25 @@ class Item(WebsiteGenerator):
def update_template_item(self):
"""Set Show in Website for Template Item if True for its Variant"""
- if self.variant_of:
- if self.show_in_website:
- self.show_variant_in_website = 1
- self.show_in_website = 0
+ if not self.variant_of:
+ return
- if self.show_variant_in_website:
- # show template
- template_item = frappe.get_doc("Item", self.variant_of)
+ if self.show_in_website:
+ self.show_variant_in_website = 1
+ self.show_in_website = 0
- if not template_item.show_in_website:
- template_item.show_in_website = 1
- template_item.flags.dont_update_variants = True
- template_item.flags.ignore_permissions = True
- template_item.save()
+ if self.show_variant_in_website:
+ # show template
+ template_item = frappe.get_doc("Item", self.variant_of)
+
+ if not template_item.show_in_website:
+ template_item.show_in_website = 1
+ template_item.flags.dont_update_variants = True
+ template_item.flags.ignore_permissions = True
+ template_item.save()
def validate_item_defaults(self):
- companies = list(set([row.company for row in self.item_defaults]))
+ companies = {row.company for row in self.item_defaults}
if len(companies) != len(self.item_defaults):
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
@@ -813,7 +813,7 @@ class Item(WebsiteGenerator):
frappe.throw(_("Item has variants."))
def validate_attributes_in_variants(self):
- if not self.has_variants or self.get("__islocal"):
+ if not self.has_variants or self.is_new():
return
old_doc = self.get_doc_before_save()
@@ -901,7 +901,7 @@ class Item(WebsiteGenerator):
frappe.throw(_("Variant Based On cannot be changed"))
def validate_uom(self):
- if not self.get("__islocal"):
+ if not self.is_new():
check_stock_uom_with_bin(self.name, self.stock_uom)
if self.has_variants:
for d in frappe.db.get_all("Item", filters={"variant_of": self.name}):
@@ -959,20 +959,20 @@ class Item(WebsiteGenerator):
d.variant_of = self.variant_of
def cant_change(self):
- if not self.get("__islocal"):
- fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
+ if self.is_new():
+ return
- values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
- if not values.get('valuation_method') and self.get('valuation_method'):
- values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
+ fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
- if values:
- for field in fields:
- if cstr(self.get(field)) != cstr(values.get(field)):
- if not self.check_if_linked_document_exists(field):
- break # no linked document, allowed
- else:
- frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
+ values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
+ if not values.get('valuation_method') and self.get('valuation_method'):
+ values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
+
+ if values:
+ for field in fields:
+ if cstr(self.get(field)) != cstr(values.get(field)):
+ if self.check_if_linked_document_exists(field):
+ frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
def check_if_linked_document_exists(self, field):
linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item",
@@ -1054,56 +1054,42 @@ def make_item_price(item, price_list_name, item_price):
}).insert()
def get_timeline_data(doctype, name):
- '''returns timeline data based on stock ledger entry'''
- out = {}
- items = dict(frappe.db.sql('''select posting_date, count(*)
- from `tabStock Ledger Entry` where item_code=%s
- and posting_date > date_sub(curdate(), interval 1 year)
- group by posting_date''', name))
+ """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page."""
- for date, count in iteritems(items):
- timestamp = get_timestamp(date)
- out.update({timestamp: count})
+ items = frappe.db.sql("""select unix_timestamp(posting_date), count(*)
+ from `tabStock Ledger Entry`
+ where item_code=%s and posting_date > date_sub(curdate(), interval 1 year)
+ group by posting_date""", name)
- return out
+ return dict(items)
-def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1):
+
+def validate_end_of_life(item_code, end_of_life=None, disabled=None):
if (not end_of_life) or (disabled is None):
end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"])
if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date():
- msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))
- _msgprint(msg, verbose)
+ frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)))
if disabled:
- _msgprint(_("Item {0} is disabled").format(item_code), verbose)
+ frappe.throw(_("Item {0} is disabled").format(item_code))
-def validate_is_stock_item(item_code, is_stock_item=None, verbose=1):
+def validate_is_stock_item(item_code, is_stock_item=None):
if not is_stock_item:
is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item")
if is_stock_item != 1:
- msg = _("Item {0} is not a stock Item").format(item_code)
-
- _msgprint(msg, verbose)
+ frappe.throw(_("Item {0} is not a stock Item").format(item_code))
-def validate_cancelled_item(item_code, docstatus=None, verbose=1):
+def validate_cancelled_item(item_code, docstatus=None):
if docstatus is None:
docstatus = frappe.db.get_value("Item", item_code, "docstatus")
if docstatus == 2:
- msg = _("Item {0} is cancelled").format(item_code)
- _msgprint(msg, verbose)
-
-def _msgprint(msg, verbose):
- if verbose:
- msgprint(msg, raise_exception=True)
- else:
- raise frappe.ValidationError(msg)
-
+ frappe.throw(_("Item {0} is cancelled").format(item_code))
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom"""
@@ -1203,27 +1189,25 @@ def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return
- matched = True
ref_uom = frappe.db.get_value("Stock Ledger Entry",
{"item_code": item}, "stock_uom")
if ref_uom:
if cstr(ref_uom) != cstr(stock_uom):
- matched = False
- else:
- bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1)
- for bin in bin_list:
- if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0
- or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom):
- matched = False
- break
+ frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item))
- if matched and bin_list:
- frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item))
+ bin_list = frappe.db.sql("""
+ select * from tabBin where item_code = %s
+ and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0)
+ and stock_uom != %s
+ """, (item, stock_uom), as_dict=1)
+
+ if bin_list:
+ frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item))
+
+ # No SLE or documents against item. Bin UOM can be changed safely.
+ frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item))
- if not matched:
- frappe.throw(
- _("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item))
def get_item_defaults(item_code, company):
item = frappe.get_cached_doc('Item', item_code)
@@ -1264,45 +1248,59 @@ def get_item_details(item_code, company=None):
@frappe.whitelist()
def get_uom_conv_factor(uom, stock_uom):
- uoms = [uom, stock_uom]
- value = ""
- uom_details = frappe.db.sql("""select to_uom, from_uom, value from `tabUOM Conversion Factor`\
- where to_uom in ({0})
- """.format(', '.join([frappe.db.escape(i, percent=False) for i in uoms])), as_dict=True)
+ """ Get UOM conversion factor from uom to stock_uom
+ e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0
+ """
+ if uom == stock_uom:
+ return 1.0
- for d in uom_details:
- if d.from_uom == stock_uom and d.to_uom == uom:
- value = 1/flt(d.value)
- elif d.from_uom == uom and d.to_uom == stock_uom:
- value = d.value
+ from_uom, to_uom = uom, stock_uom # renaming for readability
- if not value:
- uom_stock = frappe.db.get_value("UOM Conversion Factor", {"to_uom": stock_uom}, ["from_uom", "value"], as_dict=1)
- uom_row = frappe.db.get_value("UOM Conversion Factor", {"to_uom": uom}, ["from_uom", "value"], as_dict=1)
+ exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
+ if exact_match:
+ return exact_match.value
- if uom_stock and uom_row:
- if uom_stock.from_uom == uom_row.from_uom:
- value = flt(uom_stock.value) * 1/flt(uom_row.value)
+ inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1)
+ if inverse_match:
+ return 1 / inverse_match.value
+
+ # This attempts to try and get conversion from intermediate UOM.
+ # case:
+ # g -> mg = 1000
+ # g -> kg = 0.001
+ # therefore kg -> mg = 1000 / 0.001 = 1,000,000
+ intermediate_match = frappe.db.sql("""
+ select (first.value / second.value) as value
+ from `tabUOM Conversion Factor` first
+ join `tabUOM Conversion Factor` second
+ on first.from_uom = second.from_uom
+ where
+ first.to_uom = %(to_uom)s
+ and second.to_uom = %(from_uom)s
+ limit 1
+ """, {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1)
+
+ if intermediate_match:
+ return intermediate_match[0].value
- return value
@frappe.whitelist()
-def get_item_attribute(parent, attribute_value=''):
+def get_item_attribute(parent, attribute_value=""):
+ """Used for providing auto-completions in child table."""
if not frappe.has_permission("Item"):
- frappe.msgprint(_("No Permission"), raise_exception=1)
+ frappe.throw(_("No Permission"))
return frappe.get_all("Item Attribute Value", fields = ["attribute_value"],
- filters = {'parent': parent, 'attribute_value': ("like", "%%%s%%" % attribute_value)})
+ filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")})
def update_variants(variants, template, publish_progress=True):
- count=0
- for d in variants:
+ total = len(variants)
+ for count, d in enumerate(variants, start=1):
variant = frappe.get_doc("Item", d)
copy_attributes_to_variant(template, variant)
variant.save()
- count+=1
if publish_progress:
- frappe.publish_progress(count*100/len(variants), title = _("Updating Variants..."))
+ frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
def on_doctype_update():
# since route is a Text column, it needs a length for indexing
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index e0b89d8e451..c7467a5a0f5 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -10,14 +10,15 @@ from frappe.test_runner import make_test_objects
from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError,
InvalidItemAttributeValueError, get_variant)
from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode
-from erpnext.stock.doctype.item.item import get_uom_conv_factor
+from erpnext.stock.doctype.item.item import (get_uom_conv_factor, get_item_attribute,
+ validate_is_stock_item, get_timeline_data)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details
+from erpnext.tests.utils import change_settings
-from six import iteritems
test_ignore = ["BOM"]
-test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand"]
+test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
def make_item(item_code, properties=None):
if frappe.db.exists("Item", item_code):
@@ -98,7 +99,7 @@ class TestItem(unittest.TestCase):
"ignore_pricing_rule": 1
})
- for key, value in iteritems(to_check):
+ for key, value in to_check.items():
self.assertEqual(value, details.get(key))
def test_item_tax_template(self):
@@ -194,7 +195,7 @@ class TestItem(unittest.TestCase):
"plc_conversion_rate": 1,
"customer": "_Test Customer",
})
- for key, value in iteritems(sales_item_check):
+ for key, value in sales_item_check.items():
self.assertEqual(value, sales_item_details.get(key))
purchase_item_check = {
@@ -215,7 +216,7 @@ class TestItem(unittest.TestCase):
"plc_conversion_rate": 1,
"supplier": "_Test Supplier",
})
- for key, value in iteritems(purchase_item_check):
+ for key, value in purchase_item_check.items():
self.assertEqual(value, purchase_item_details.get(key))
def test_item_attribute_change_after_variant(self):
@@ -375,6 +376,14 @@ class TestItem(unittest.TestCase):
self.assertEqual(item_doc.uoms[1].uom, "Kg")
self.assertEqual(item_doc.uoms[1].conversion_factor, 1000)
+ def test_uom_conv_intermediate(self):
+ factor = get_uom_conv_factor("Pound", "Gram")
+ self.assertAlmostEqual(factor, 453.592, 3)
+
+ def test_uom_conv_base_case(self):
+ factor = get_uom_conv_factor("m", "m")
+ self.assertEqual(factor, 1.0)
+
def test_item_variant_by_manufacturer(self):
fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}]
set_item_variant_settings(fields)
@@ -464,7 +473,7 @@ class TestItem(unittest.TestCase):
self.assertEqual(len(matching_barcodes), 1)
details = matching_barcodes[0]
- for key, value in iteritems(barcode_properties):
+ for key, value in barcode_properties.items():
self.assertEqual(value, details.get(key))
# Add barcode again - should cause DuplicateEntryError
@@ -480,6 +489,89 @@ class TestItem(unittest.TestCase):
new_barcode.barcode_type = 'EAN'
self.assertRaises(InvalidBarcode, item_doc.save)
+ def test_heatmap_data(self):
+ import time
+ data = get_timeline_data("Item", "_Test Item")
+ self.assertTrue(isinstance(data, dict))
+
+ now = time.time()
+ one_year_ago = now - 366 * 24 * 60 * 60
+
+ for timestamp, count in data.items():
+ self.assertIsInstance(timestamp, int)
+ self.assertTrue(one_year_ago <= timestamp <= now)
+ self.assertIsInstance(count, int)
+ self.assertTrue(count >= 0)
+
+ def test_index_creation(self):
+ "check if index is getting created in db"
+ from erpnext.stock.doctype.item.item import on_doctype_update
+ on_doctype_update()
+
+ indices = frappe.db.sql("show index from tabItem", as_dict=1)
+ expected_columns = {"item_code", "item_name", "item_group", "route"}
+ for index in indices:
+ expected_columns.discard(index.get("Column_name"))
+
+ if expected_columns:
+ self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
+
+ def test_attribute_completions(self):
+ expected_attrs = {"Small", "Extra Small", "Extra Large", "Large", "2XL", "Medium"}
+
+ attrs = get_item_attribute("Test Size")
+ received_attrs = {attr.attribute_value for attr in attrs}
+ self.assertEqual(received_attrs, expected_attrs)
+
+ attrs = get_item_attribute("Test Size", attribute_value="extra")
+ received_attrs = {attr.attribute_value for attr in attrs}
+ self.assertEqual(received_attrs, {"Extra Small", "Extra Large"})
+
+ def test_check_stock_uom_with_bin(self):
+ # this item has opening stock and stock_uom set in test_records.
+ item = frappe.get_doc("Item", "_Test Item")
+ item.stock_uom = "Gram"
+ self.assertRaises(frappe.ValidationError, item.save)
+
+ def test_check_stock_uom_with_bin_no_sle(self):
+ from erpnext.stock.stock_balance import update_bin_qty
+ item = create_item("_Item with bin qty")
+ item.stock_uom = "Gram"
+ item.save()
+
+ update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
+ "reserved_qty": 10
+ })
+
+ item.stock_uom = "Kilometer"
+ self.assertRaises(frappe.ValidationError, item.save)
+
+ update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
+ "reserved_qty": 0
+ })
+
+ item.load_from_db()
+ item.stock_uom = "Kilometer"
+ try:
+ item.save()
+ except frappe.ValidationError as e:
+ self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
+
+ def test_validate_stock_item(self):
+ self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
+
+ try:
+ validate_is_stock_item("_Test Item")
+ except frappe.ValidationError as e:
+ self.fail(f"stock item considered non-stock item: {e}")
+
+ @change_settings("Stock Settings", {"item_naming_by": "Naming Series"})
+ def test_autoname_series(self):
+ item = frappe.new_doc("Item")
+ item.item_group = "All Item Groups"
+ item.save() # if item code saved without item_code then series worked
+
+
def set_item_variant_settings(fields):
doc = frappe.get_doc('Item Variant Settings')
doc.set('fields', fields)
@@ -494,23 +586,24 @@ def make_item_variant():
test_records = frappe.get_test_records('Item')
-def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None,
- customer=None, is_purchase_item=None, opening_stock=None, company=None):
+def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC",
+ is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0,
+ company="_Test Company"):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
item.item_name = item_code
item.description = item_code
item.item_group = "All Item Groups"
- item.is_stock_item = is_stock_item or 1
- item.opening_stock = opening_stock or 0
- item.valuation_rate = valuation_rate or 0.0
+ item.is_stock_item = is_stock_item
+ item.opening_stock = opening_stock
+ item.valuation_rate = valuation_rate
item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or ''
item.append("item_defaults", {
- "default_warehouse": warehouse or '_Test Warehouse - _TC',
- "company": company or "_Test Company"
+ "default_warehouse": warehouse,
+ "company": company
})
item.save()
else:
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index d5700fe5147..8f76844bde0 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase):
make_items()
def test_alternative_item_for_subcontract_rm(self):
+ frappe.db.set_value('Buying Settings', None,
+ 'backflush_raw_materials_of_subcontract_based_on', 'BOM')
+
create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC',
qty=5, rate=2000)
create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
@@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase):
status = True
self.assertEqual(status, True)
+ frappe.db.set_value('Buying Settings', None,
+ 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract')
def test_alternative_item_for_production_rm(self):
create_stock_reconciliation(item_code='Alternate Item For A RW 1',
diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json
index ae36efc7e3f..fb100967f3b 100644
--- a/erpnext/stock/doctype/item_tax/item_tax.json
+++ b/erpnext/stock/doctype/item_tax/item_tax.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:01",
"doctype": "DocType",
"editable_grid": 1,
@@ -6,7 +7,9 @@
"field_order": [
"item_tax_template",
"tax_category",
- "valid_from"
+ "valid_from",
+ "minimum_net_rate",
+ "maximum_net_rate"
],
"fields": [
{
@@ -33,11 +36,24 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid From"
+ },
+ {
+ "fieldname": "maximum_net_rate",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Maximum Net Rate"
+ },
+ {
+ "fieldname": "minimum_net_rate",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Minimum Net Rate"
}
],
"idx": 1,
"istable": 1,
- "modified": "2020-06-25 01:40:28.859752",
+ "links": [],
+ "modified": "2021-06-03 13:20:06.982303",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Tax",
diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
index 4fcdb4c10cc..9c59c13ac07 100644
--- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
+++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
@@ -10,8 +10,8 @@
"exchange_rate",
"description",
"col_break3",
- "base_amount",
- "amount"
+ "amount",
+ "base_amount"
],
"fields": [
{
@@ -59,7 +59,7 @@
{
"fieldname": "base_amount",
"fieldtype": "Currency",
- "label": "Base Amount",
+ "label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
}
@@ -67,7 +67,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-26 01:07:23.233604",
+ "modified": "2021-05-17 13:57:10.807980",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 83109469fc7..5df4d8743fc 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -78,7 +78,7 @@ class LandedCostVoucher(Document):
.format(item.idx, item.item_code))
def set_total_taxes_and_charges(self):
- self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")])
+ self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes"))
def set_applicable_charges_on_item(self):
if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually':
@@ -104,15 +104,15 @@ class LandedCostVoucher(Document):
based_on = self.distribute_charges_based_on.lower()
if based_on != 'distribute manually':
- total = sum([flt(d.get(based_on)) for d in self.get("items")])
+ total = sum(flt(d.get(based_on)) for d in self.get("items"))
else:
# consider for proportion while distributing manually
- total = sum([flt(d.get('applicable_charges')) for d in self.get("items")])
+ total = sum(flt(d.get('applicable_charges')) for d in self.get("items"))
if not total:
frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on))
- total_applicable_charges = sum([flt(d.applicable_charges) for d in self.get("items")])
+ total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items"))
precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"),
currency=frappe.get_cached_value('Company', self.company, "default_currency"))
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 984ae46c66c..32b08f60c4a 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -311,7 +311,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
def distribute_landed_cost_on_items(lcv):
based_on = lcv.distribute_charges_based_on.lower()
- total = sum([flt(d.get(based_on)) for d in lcv.get("items")])
+ total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
for item in lcv.get("items"):
item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total)
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 92c8d213878..6e66f9869c0 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', {
}
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
- if (flt(frm.doc.per_ordered, 2) < 100) {
+ let precision = frappe.defaults.get_default("float_precision");
+ if (flt(frm.doc.per_ordered, precision) < 100) {
let add_create_pick_list_button = () => {
frm.add_custom_button(__('Pick List'),
() => frm.events.create_pick_list(frm), __('Create'));
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index f1d7f8c8c9e..bb396e806f6 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -13,6 +13,7 @@
"section_break_6",
"warehouse",
"target_warehouse",
+ "conversion_factor",
"column_break_9",
"qty",
"uom",
@@ -209,13 +210,18 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-24 09:25:13.050151",
+ "modified": "2021-05-26 07:08:05.111385",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 5341f298531..4ab71bdf629 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -53,6 +53,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip
pi.parent_detail_docname = main_item_row.name
pi.uom = item.stock_uom
pi.qty = flt(qty)
+ pi.conversion_factor = main_item_row.conversion_factor
if description and not pi.description:
pi.description = description
if not pi.warehouse and not doc.amended_from:
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index 2008bffcd32..4a843e0fde9 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -88,9 +88,9 @@ class PackingSlip(Document):
rows = [d.item_code for d in self.get("items")]
# also pick custom fields from delivery note
- custom_fields = ', '.join(['dni.`{0}`'.format(d.fieldname)
+ custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname)
for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
- if d.fieldtype not in no_value_fields])
+ if d.fieldtype not in no_value_fields)
if custom_fields:
custom_fields = ', ' + custom_fields
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 32d349f3031..44fb736304f 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -514,8 +514,7 @@
"oldfieldname": "pr_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Receipt Item Supplied",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "section_break0",
@@ -762,6 +761,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
@@ -805,6 +805,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -1147,7 +1148,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-26 20:49:39.106049",
+ "modified": "2021-05-25 00:15:12.239017",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 61e60f39228..82c87a83a50 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
+ self.set_consumed_qty_in_po()
def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name
@@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
+ self.set_consumed_qty_in_po()
@frappe.whitelist()
def get_current_stock(self):
@@ -243,16 +245,23 @@ class PurchaseReceipt(BuyingController):
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
+ gl_entries = []
+ self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
+ self.make_tax_gl_entries(gl_entries)
+ self.get_asset_gl_entry(gl_entries)
+
+ return process_gl_map(gl_entries)
+
+ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
- gl_entries = []
warehouse_with_no_account = []
- negative_expense_to_be_booked = 0.0
stock_items = self.get_stock_items()
+
for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
@@ -263,21 +272,22 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff:
continue
+ warehouse_account_name = warehouse_account[d.warehouse]["account"]
+ warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
+ supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
+ supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency")
+ remarks = self.get("remarks") or _("Accounting Entry for Stock")
+
# If PR is sub-contracted and fg item rate is zero
- # in that case if account for shource and target warehouse are same,
+ # in that case if account for source and target warehouse are same,
# then GL entries should not be posted
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
and warehouse_account.get(self.supplier_warehouse) \
- and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]:
+ and warehouse_account_name == supplier_warehouse_account:
continue
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[d.warehouse]["account"],
- "against": stock_rbnb,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": stock_value_diff
- }, warehouse_account[d.warehouse]["account_currency"], item=d))
+ self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -287,43 +297,28 @@ class PurchaseReceipt(BuyingController):
credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount:
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[d.from_warehouse]['account'] \
- if d.from_warehouse else stock_rbnb,
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")),
- "debit_in_account_currency": -1 * credit_amount
- }, credit_currency, item=d))
+ account = warehouse_account[d.from_warehouse]['account'] \
+ if d.from_warehouse else stock_rbnb
- negative_expense_to_be_booked += flt(d.item_tax_amount)
+ self.add_gl_entry(gl_entries, account, d.cost_center,
+ -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
+ debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
- # Amount added through landed-cost-voucher
+ # Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
account_currency = get_account_currency(account)
- gl_entries.append(self.get_gl_dict({
- "account": account,
- "account_currency": account_currency,
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
- account_currency!=self.company_currency) else flt(amount["amount"])),
- "credit_in_account_currency": flt(amount["amount"]),
- "project": d.project
- }, item=d))
+ credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or
+ account_currency!=self.company_currency) else flt(amount["amount"]))
+
+ self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks,
+ warehouse_account_name, credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency, project=d.project, item=d)
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[self.supplier_warehouse]["account"],
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(d.rm_supp_cost)
- }, warehouse_account[self.supplier_warehouse]["account_currency"], item=d))
+ self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost),
+ remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d)
# divisional loss adjustment
valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \
@@ -340,46 +335,32 @@ class PurchaseReceipt(BuyingController):
cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
- gl_entries.append(self.get_gl_dict({
- "account": loss_account,
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": divisional_loss,
- "project": d.project
- }, credit_currency, item=d))
+ self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks,
+ warehouse_account_name, account_currency=credit_currency, project=d.project, item=d)
elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse)
elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
-
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account)
-
- gl_entries.append(self.get_gl_dict({
- "account": service_received_but_not_billed_account,
- "against": d.expense_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Service"),
- "project": d.project,
- "credit": d.amount,
- "voucher_detail_no": d.name
- }, credit_currency, item=d))
-
debit_currency = get_account_currency(d.expense_account)
+ remarks = self.get("remarks") or _("Accounting Entry for Service")
- gl_entries.append(self.get_gl_dict({
- "account": d.expense_account,
- "against": service_received_but_not_billed_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Service"),
- "project": d.project,
- "debit": d.amount,
- "voucher_detail_no": d.name
- }, debit_currency, item=d))
+ self.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount,
+ remarks, d.expense_account, account_currency=credit_currency, project=d.project,
+ voucher_detail_no=d.name, item=d)
- self.get_asset_gl_entry(gl_entries)
+ self.add_gl_entry(gl_entries, d.expense_account, d.cost_center, d.amount, 0.0, remarks, service_received_but_not_billed_account,
+ account_currency = debit_currency, project=d.project, voucher_detail_no=d.name, item=d)
+
+ if warehouse_with_no_account:
+ frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
+ "\n".join(warehouse_with_no_account))
+
+ def make_tax_gl_entries(self, gl_entries):
+ expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+ negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')])
# Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {}
for tax in self.get("taxes"):
@@ -405,6 +386,7 @@ class PurchaseReceipt(BuyingController):
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
+ stock_rbnb = self.get_company_default("stock_received_but_not_billed")
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
@@ -420,23 +402,33 @@ class PurchaseReceipt(BuyingController):
applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
amount_including_divisional_loss -= applicable_amount
- gl_entries.append(
- self.get_gl_dict({
- "account": account,
- "cost_center": tax.cost_center,
- "credit": applicable_amount,
- "remarks": self.remarks or _("Accounting Entry for Stock"),
- "against": against_account
- }, item=tax)
- )
+ self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"),
+ against_account, item=tax)
i += 1
- if warehouse_with_no_account:
- frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
- "\n".join(warehouse_with_no_account))
+ def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
+ debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
+ project=None, voucher_detail_no=None, item=None):
+ gl_entry = {
+ "account": account,
+ "cost_center": cost_center,
+ "debit": debit,
+ "credit": credit,
+ "against_account": against_account,
+ "remarks": remarks,
+ }
- return process_gl_map(gl_entries)
+ if voucher_detail_no:
+ gl_entry.update({"voucher_detail_no": voucher_detail_no})
+
+ if debit_in_account_currency:
+ gl_entry.update({"debit_in_account_currency": debit_in_account_currency})
+
+ if credit_in_account_currency:
+ gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
+
+ gl_entries.append(self.get_gl_dict(gl_entry, item=item))
def get_asset_gl_entry(self, gl_entries):
for item in self.get("items"):
@@ -458,30 +450,21 @@ class PurchaseReceipt(BuyingController):
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
+ remarks = self.get("remarks") or _("Accounting Entry for Asset")
cwip_account_currency = get_account_currency(cwip_account)
# debit cwip account
- gl_entries.append(self.get_gl_dict({
- "account": cwip_account,
- "against": arbnb_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "debit": base_asset_amount,
- "debit_in_account_currency": (base_asset_amount
- if cwip_account_currency == self.company_currency else asset_amount)
- }, item=item))
+ debit_in_account_currency = (base_asset_amount
+ if cwip_account_currency == self.company_currency else asset_amount)
+ self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks,
+ arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item)
asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account
- gl_entries.append(self.get_gl_dict({
- "account": arbnb_account,
- "against": cwip_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "credit": base_asset_amount,
- "credit_in_account_currency": (base_asset_amount
- if asset_rbnb_currency == self.company_currency else asset_amount)
- }, item=item))
+ credit_in_account_currency = (base_asset_amount
+ if asset_rbnb_currency == self.company_currency else asset_amount)
+ self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks,
+ cwip_account, credit_in_account_currency=credit_in_account_currency, item=item)
def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -492,23 +475,13 @@ class PurchaseReceipt(BuyingController):
# This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
- gl_entries.append(self.get_gl_dict({
- "account": expenses_included_in_asset_valuation,
- "against": asset_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(item.landed_cost_voucher_amount),
- "project": item.project
- }, item=item))
+ remarks = self.get("remarks") or _("Accounting Entry for Stock")
- gl_entries.append(self.get_gl_dict({
- "account": asset_account,
- "against": expenses_included_in_asset_valuation,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": flt(item.landed_cost_voucher_amount),
- "project": item.project
- }, item=item))
+ self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
+ remarks, asset_account, project=item.project, item=item)
+
+ self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
+ remarks, expenses_included_in_asset_valuation, project=item.project, item=item)
def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all('Asset',
@@ -527,7 +500,7 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
for d in self.get("items"):
- if d.purchase_invoice and d.purchase_invoice_item:
+ if d.get("purchase_invoice") and d.get("purchase_invoice_item"):
d.db_set('billed_amt', d.amount, update_modified=update_modified)
elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified)
@@ -609,7 +582,6 @@ def update_billing_percentage(pr_doc, update_modified=True):
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
- from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc('Purchase Receipt', source_name)
@@ -629,11 +601,16 @@ def make_purchase_invoice(source_name, target_doc=None):
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ target_doc.rejected_qty = 0
target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
- pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
+ qty = item_row.qty
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ qty = item_row.received_qty
+ pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
if returned_qty:
if returned_qty >= pending_qty:
@@ -778,4 +755,3 @@ def get_item_account_wise_additional_cost(purchase_document):
account.base_amount * item.get(based_on_field) / total_item_cost
return item_account_wise_cost
-
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index e5ef978ca3c..2eb8bfd5d2f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -246,7 +246,7 @@ class TestPurchaseReceipt(unittest.TestCase):
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes")
self.assertEqual(len(pr.get("supplied_items")), 2)
- rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
+ rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
pr.cancel()
@@ -297,6 +297,8 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = "Test Extra Item 1", qty=10, basic_rate=100)
se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
+ se3 = make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 2", qty=1, basic_rate=100)
rm_items = [
{
"item_code": item_code,
@@ -331,7 +333,12 @@ class TestPurchaseReceipt(unittest.TestCase):
se.cancel()
se1.cancel()
se2.cancel()
+ se3.cancel()
po.reload()
+ pr2.load_from_db()
+ pr2.cancel()
+
+ po.load_from_db()
po.cancel()
def test_serial_no_supplier(self):
@@ -414,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(return_pr_2.items[0].qty, -3)
# Make PI against unreturned amount
+ buying_settings = frappe.get_single("Buying Settings")
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
+ buying_settings.save()
+
pi = make_purchase_invoice(pr.name)
pi.submit()
self.assertEqual(pi.items[0].qty, 3)
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1
+ buying_settings.save()
+
pr.load_from_db()
# PR should be completed on billing all unreturned amount
self.assertEqual(pr.items[0].billed_amt, 150)
@@ -997,6 +1011,47 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr.status, "To Bill")
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
+ def test_service_item_purchase_with_perpetual_inventory(self):
+ company = '_Test Company with perpetual inventory'
+ service_item = '_Test Non Stock Item'
+
+ before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items')
+ frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1)
+ srbnb_account = 'Stock Received But Not Billed - TCP1'
+ frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account)
+
+ pr = make_purchase_receipt(
+ company=company, item=service_item,
+ warehouse='Finished Goods - TCP1', do_not_save=1
+ )
+ item_row_with_diff_rate = frappe.copy_doc(pr.items[0])
+ item_row_with_diff_rate.rate = 100
+ pr.append('items', item_row_with_diff_rate)
+
+ pr.save()
+ pr.submit()
+
+ item_one_gl_entry = frappe.db.get_all("GL Entry", {
+ 'voucher_type': pr.doctype,
+ 'voucher_no': pr.name,
+ 'account': srbnb_account,
+ 'voucher_detail_no': pr.items[0].name
+ }, pluck="name")
+
+ item_two_gl_entry = frappe.db.get_all("GL Entry", {
+ 'voucher_type': pr.doctype,
+ 'voucher_no': pr.name,
+ 'account': srbnb_account,
+ 'voucher_detail_no': pr.items[1].name
+ }, pluck="name")
+
+ # check if the entries are not merged into one
+ # seperate entries should be made since voucher_detail_no is different
+ self.assertEqual(len(item_one_gl_entry), 1)
+ self.assertEqual(len(item_two_gl_entry), 1)
+
+ frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 56b046a92e1..7f3d701034a 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -1,29 +1,45 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-from __future__ import unicode_literals
-import frappe
import unittest
+
+import frappe
from frappe.utils import nowdate
-from erpnext.stock.doctype.item.test_item import create_item
+
+from erpnext.controllers.stock_controller import (
+ QualityInspectionNotSubmittedError,
+ QualityInspectionRejectedError,
+ QualityInspectionRequiredError,
+ make_quality_inspections,
+)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError
# test_records = frappe.get_test_records('Quality Inspection')
+
class TestQualityInspection(unittest.TestCase):
def setUp(self):
create_item("_Test Item with QA")
- frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1)
+ frappe.db.set_value(
+ "Item", "_Test Item with QA", "inspection_required_before_delivery", 1
+ )
def test_qa_for_delivery(self):
- make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100)
+ make_stock_entry(
+ item_code="_Test Item with QA",
+ target="_Test Warehouse - _TC",
+ qty=1,
+ basic_rate=100
+ )
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
self.assertRaises(QualityInspectionRequiredError, dn.submit)
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected")
+ qa = create_quality_inspection(
+ reference_type="Delivery Note", reference_name=dn.name, status="Rejected"
+ )
dn.reload()
self.assertRaises(QualityInspectionRejectedError, dn.submit)
@@ -38,7 +54,9 @@ class TestQualityInspection(unittest.TestCase):
def test_qa_not_submit(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True)
+ qa = create_quality_inspection(
+ reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True
+ )
dn.items[0].quality_inspection = qa.name
self.assertRaises(QualityInspectionNotSubmittedError, dn.submit)
@@ -48,21 +66,28 @@ class TestQualityInspection(unittest.TestCase):
def test_value_based_qi_readings(self):
# Test QI based on acceptance values (Non formula)
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
- readings = [{
- "specification": "Iron Content", # numeric reading
- "min_value": 0.1,
- "max_value": 0.9,
- "reading_1": "0.4"
- },
- {
- "specification": "Particle Inspection Needed", # non-numeric reading
- "numeric": 0,
- "value": "Yes",
- "reading_value": "Yes"
- }]
+ readings = [
+ {
+ "specification": "Iron Content", # numeric reading
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ },
+ {
+ "specification": "Particle Inspection Needed", # non-numeric reading
+ "numeric": 0,
+ "value": "Yes",
+ "reading_value": "Yes"
+ }
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Delivery Note",
+ reference_name=dn.name,
+ readings=readings,
+ do_not_save=True
+ )
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
- readings=readings, do_not_save=True)
qa.save()
# status must be auto set as per formula
@@ -74,36 +99,43 @@ class TestQualityInspection(unittest.TestCase):
def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
- readings = [{
- "specification": "Iron Content", # numeric reading
- "formula_based_criteria": 1,
- "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
- "reading_1": "0.4"
- },
- {
- "specification": "Calcium Content", # numeric reading
- "formula_based_criteria": 1,
- "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
- "reading_1": "0.7"
- },
- {
- "specification": "Mg Content", # numeric reading
- "formula_based_criteria": 1,
- "acceptance_formula": "mean < 0.9",
- "reading_1": "0.5",
- "reading_2": "0.7",
- "reading_3": "random text" # check if random string input causes issues
- },
- {
- "specification": "Calcium Content", # non-numeric reading
- "formula_based_criteria": 1,
- "numeric": 0,
- "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
- "reading_value": "Grade B"
- }]
+ readings = [
+ {
+ "specification": "Iron Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
+ "reading_1": "0.4"
+ },
+ {
+ "specification": "Calcium Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
+ "reading_1": "0.7"
+ },
+ {
+ "specification": "Mg Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "mean < 0.9",
+ "reading_1": "0.5",
+ "reading_2": "0.7",
+ "reading_3": "random text" # check if random string input causes issues
+ },
+ {
+ "specification": "Calcium Content", # non-numeric reading
+ "formula_based_criteria": 1,
+ "numeric": 0,
+ "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
+ "reading_value": "Grade B"
+ }
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Delivery Note",
+ reference_name=dn.name,
+ readings=readings,
+ do_not_save=True
+ )
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
- readings=readings, do_not_save=True)
qa.save()
# status must be auto set as per formula
@@ -115,6 +147,19 @@ class TestQualityInspection(unittest.TestCase):
qa.delete()
dn.delete()
+ def test_make_quality_inspections_from_linked_document(self):
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ for item in dn.items:
+ item.sample_size = item.qty
+ quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items)
+ self.assertEqual(len(dn.items), len(quality_inspections))
+
+ # cleanup
+ for qi in quality_inspections:
+ frappe.delete_doc("Quality Inspection", qi)
+ dn.delete()
+
+
def create_quality_inspection(**args):
args = frappe._dict(args)
qa = frappe.new_doc("Quality Inspection")
@@ -134,7 +179,7 @@ def create_quality_inspection(**args):
readings = args.readings
if args.status == "Rejected":
- readings["reading_1"] = "12" # status is auto set in child on save
+ readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list):
for entry in readings:
@@ -150,10 +195,11 @@ def create_quality_inspection(**args):
return qa
+
def create_quality_inspection_parameter(parameter):
if not frappe.db.exists("Quality Inspection Parameter", parameter):
frappe.get_doc({
"doctype": "Quality Inspection Parameter",
"parameter": parameter,
"description": parameter
- }).insert()
\ No newline at end of file
+ }).insert()
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 63c71891e44..55f2ebb2241 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -4,8 +4,9 @@
from __future__ import unicode_literals
import frappe, erpnext
+from rq.timeouts import JobTimeoutException
from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form, add_to_date, now, today
+from frappe.utils import cint, get_link_to_form, add_to_date, now, today, time_diff_in_hours
from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role
@@ -36,6 +37,9 @@ class RepostItemValuation(Document):
self.db_set('status', status)
def on_submit(self):
+ if not frappe.flags.in_test:
+ return
+
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=frappe.flags.in_test, doc=self)
@@ -57,7 +61,8 @@ def repost(doc):
repost_gl_entries(doc)
doc.set_status('Completed')
- except Exception:
+
+ except (Exception, JobTimeoutException):
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(traceback)
@@ -127,9 +132,7 @@ def repost_entries():
check_if_stock_and_account_balance_synced(today(), d.name)
def get_repost_item_valuation_entries():
- date = add_to_date(now(), hours=-3)
-
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
WHERE status != 'Completed' and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
- """, date, as_dict=1)
\ No newline at end of file
+ """, now(), as_dict=1)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index 3acf3a9316c..a3d44af4945 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -57,7 +57,8 @@
"more_info",
"serial_no_details",
"company",
- "status"
+ "status",
+ "work_order"
],
"fields": [
{
@@ -422,12 +423,18 @@
"label": "Status",
"options": "\nActive\nInactive\nDelivered\nExpired",
"read_only": 1
+ },
+ {
+ "fieldname": "work_order",
+ "fieldtype": "Link",
+ "label": "Work Order",
+ "options": "Work Order"
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2020-07-20 20:50:16.660433",
+ "modified": "2021-01-08 14:31:15.375996",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 5ecc9f81405..bad7b608acf 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -473,16 +473,13 @@ def get_serial_nos(serial_no):
if s.strip()]
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
- serial_no_doc.update({
- "item_code": args.get("item_code"),
- "company": args.get("company"),
- "batch_no": args.get("batch_no"),
- "via_stock_ledger": args.get("via_stock_ledger") or True,
- "supplier": args.get("supplier"),
- "location": args.get("location"),
- "warehouse": (args.get("warehouse")
- if args.get("actual_qty", 0) > 0 else None)
- })
+ for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
+ if args.get(field):
+ serial_no_doc.set(field, args.get(field))
+
+ serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
+ serial_no_doc.warehouse = (args.get("warehouse")
+ if args.get("actual_qty", 0) > 0 else None)
if is_new:
serial_no_doc.serial_no = serial_no
@@ -613,7 +610,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]):
batch_nos = filters.get("batch_no")
expiry_date = filters.get("expiry_date")
if batch_nos:
- batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join(["'%s'" % d for d in batch_nos]))
+ batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos))
if expiry_date:
batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 772c8df96e1..67083930272 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -107,6 +107,7 @@ frappe.ui.form.on('Stock Entry', {
frappe.flags.hide_serial_batch_dialog = true;
}
});
+ attach_bom_items(frm.doc.bom_no);
},
setup_quality_inspection: function(frm) {
@@ -114,6 +115,14 @@ frappe.ui.form.on('Stock Entry', {
return;
}
+ if (!frm.is_new() && frm.doc.docstatus === 0) {
+ frm.add_custom_button(__("Quality Inspection(s)"), () => {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.make_quality_inspection();
+ }, __("Create"));
+ frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+
let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) {
if (frm.is_new()) return;
@@ -154,7 +163,7 @@ frappe.ui.form.on('Stock Entry', {
refresh: function(frm) {
if(!frm.doc.docstatus) {
frm.trigger('validate_purpose_consumption');
- frm.add_custom_button(__('Create Material Request'), function() {
+ frm.add_custom_button(__('Material Request'), function() {
frappe.model.with_doctype('Material Request', function() {
var mr = frappe.model.get_new_doc('Material Request');
var items = frm.get_field('items').grid.get_selected_children();
@@ -177,7 +186,7 @@ frappe.ui.form.on('Stock Entry', {
});
frappe.set_route('Form', 'Material Request', mr.name);
});
- });
+ }, __("Create"));
}
if(frm.doc.items) {
@@ -311,6 +320,7 @@ frappe.ui.form.on('Stock Entry', {
}
frm.trigger("setup_quality_inspection");
+ attach_bom_items(frm.doc.bom_no)
},
stock_entry_type: function(frm){
@@ -598,7 +608,6 @@ frappe.ui.form.on('Stock Entry', {
add_to_transit: function(frm) {
if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') {
frm.set_value('to_warehouse', '');
- frm.set_value('stock_entry_type', 'Material Transfer');
frm.fields_dict.to_warehouse.get_query = function() {
return {
filters:{
@@ -608,12 +617,13 @@ frappe.ui.form.on('Stock Entry', {
}
};
};
- frm.trigger('set_tansit_warehouse');
+ frm.trigger('set_transit_warehouse');
}
},
- set_tansit_warehouse: function(frm) {
- if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) {
+ set_transit_warehouse: function(frm) {
+ if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse
+ && frm.doc.from_warehouse) {
let dt = frm.doc.from_warehouse ? 'Warehouse' : 'Company';
let dn = frm.doc.from_warehouse ? frm.doc.from_warehouse : frm.doc.company;
frappe.db.get_value(dt, dn, 'default_in_transit_warehouse', (r) => {
@@ -919,6 +929,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
method: "get_items",
callback: function(r) {
if(!r.exc) refresh_field("items");
+ if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no)
}
});
}
@@ -975,14 +986,17 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
- this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
+
+ if (!(row.expense_account && row.cost_center)) {
+ this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
+ }
if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
},
from_warehouse: function(doc) {
- this.frm.trigger('set_tansit_warehouse');
+ this.frm.trigger('set_transit_warehouse');
this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse);
},
@@ -1064,4 +1078,26 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
}
+function attach_bom_items(bom_no) {
+ if (!bom_no) {
+ return
+ }
+
+ if (check_should_not_attach_bom_items(bom_no)) return
+ frappe.db.get_doc("BOM",bom_no).then(bom => {
+ const {name, items} = bom
+ erpnext.stock.bom = {name, items:{}}
+ items.forEach(item => {
+ erpnext.stock.bom.items[item.item_code] = item;
+ });
+ });
+}
+
+function check_should_not_attach_bom_items(bom_no) {
+ return (
+ bom_no === undefined ||
+ (erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
+ );
+}
+
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 98c047a09ed..523d332b8f4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -59,10 +59,6 @@
"supplier_name",
"supplier_address",
"address_display",
- "column_break_39",
- "customer",
- "customer_name",
- "customer_address",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -78,7 +74,8 @@
"total_amount",
"job_card",
"amended_from",
- "credit_note"
+ "credit_note",
+ "is_return"
],
"fields": [
{
@@ -435,13 +432,13 @@
},
{
"collapsible": 1,
- "depends_on": "eval: in_list([\"Sales Return\", \"Purchase Return\", \"Send to Subcontractor\"], doc.purpose)",
+ "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"",
"fieldname": "contact_section",
"fieldtype": "Section Break",
- "label": "Customer or Supplier Details"
+ "label": "Supplier Details"
},
{
- "depends_on": "eval:doc.purpose==\"Purchase Return\" || doc.purpose==\"Send to Subcontractor\"",
+ "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"",
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
@@ -453,7 +450,7 @@
},
{
"bold": 1,
- "depends_on": "eval:doc.purpose==\"Purchase Return\" || doc.purpose==\"Send to Subcontractor\"",
+ "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"",
"fieldname": "supplier_name",
"fieldtype": "Data",
"label": "Supplier Name",
@@ -463,7 +460,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.purpose==\"Purchase Return\" || doc.purpose==\"Send to Subcontractor\"",
+ "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"",
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Supplier Address",
@@ -477,41 +474,6 @@
"fieldtype": "Small Text",
"label": "Address"
},
- {
- "fieldname": "column_break_39",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.purpose==\"Sales Return\"",
- "fieldname": "customer",
- "fieldtype": "Link",
- "label": "Customer",
- "no_copy": 1,
- "oldfieldname": "customer",
- "oldfieldtype": "Link",
- "options": "Customer",
- "print_hide": 1
- },
- {
- "bold": 1,
- "depends_on": "eval:doc.purpose==\"Sales Return\"",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "label": "Customer Name",
- "no_copy": 1,
- "oldfieldname": "customer_name",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.purpose==\"Sales Return\"",
- "fieldname": "customer_address",
- "fieldtype": "Small Text",
- "label": "Customer Address",
- "no_copy": 1,
- "oldfieldname": "customer_address",
- "oldfieldtype": "Small Text"
- },
{
"collapsible": 1,
"fieldname": "printing_settings",
@@ -637,6 +599,8 @@
{
"default": "0",
"depends_on": "eval: doc.purpose=='Material Transfer' && !doc.outgoing_stock_entry",
+ "fetch_from": "stock_entry_type.add_to_transit",
+ "fetch_if_empty": 1,
"fieldname": "add_to_transit",
"fieldtype": "Check",
"label": "Add to Transit",
@@ -648,6 +612,16 @@
"fieldname": "apply_putaway_rule",
"fieldtype": "Check",
"label": "Apply Putaway Rule"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_return",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Return",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -655,7 +629,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-09 14:58:13.267321",
+ "modified": "2021-05-26 17:07:58.015737",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 2f76bc7d56a..8f27ef4356c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -97,8 +97,7 @@ class StockEntry(StockController):
update_serial_nos_after_submit(self, "items")
self.update_work_order()
self.validate_purchase_order()
- if self.purchase_order and self.purpose == "Send to Subcontractor":
- self.update_purchase_order_supplied_items()
+ self.update_purchase_order_supplied_items()
self.make_gl_entries()
@@ -117,9 +116,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status('Completed')
def on_cancel(self):
-
- if self.purchase_order and self.purpose == "Send to Subcontractor":
- self.update_purchase_order_supplied_items()
+ self.update_purchase_order_supplied_items()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status()
@@ -465,7 +462,7 @@ class StockEntry(StockController):
"""
# Set rate for outgoing items
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
- finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item])
+ finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
# Set basic rate for incoming items
for d in self.get('items'):
@@ -501,6 +498,7 @@ class StockEntry(StockController):
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
outgoing_items_cost += flt(d.basic_amount)
+
return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
@@ -857,6 +855,7 @@ class StockEntry(StockController):
pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty")
+ pro_doc.update_batch_produced_qty(self)
if not pro_doc.operations:
pro_doc.set_actual_dates()
@@ -1008,10 +1007,12 @@ class StockEntry(StockController):
if self.purchase_order and self.purpose == "Send to Subcontractor":
#Get PO Supplied Items Details
item_wh = frappe._dict(frappe.db.sql("""
- select rm_item_code, reserve_warehouse
- from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
- where po.name = poitemsup.parent
- and po.name = %s""",self.purchase_order))
+ SELECT
+ rm_item_code, reserve_warehouse
+ FROM
+ `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
+ WHERE
+ po.name = poitemsup.parent and po.name = %s """,self.purchase_order))
for item in itervalues(item_dict):
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
@@ -1077,18 +1078,54 @@ class StockEntry(StockController):
# in case of BOM
to_warehouse = item.get("default_warehouse")
+ args = {
+ "to_warehouse": to_warehouse,
+ "from_warehouse": "",
+ "qty": self.fg_completed_qty,
+ "item_name": item.item_name,
+ "description": item.description,
+ "stock_uom": item.stock_uom,
+ "expense_account": item.get("expense_account"),
+ "cost_center": item.get("buying_cost_center"),
+ "is_finished_item": 1
+ }
+
+ if self.work_order and self.pro_doc.has_batch_no:
+ self.set_batchwise_finished_goods(args, item)
+ else:
+ self.add_finisged_goods(args, item)
+
+ def set_batchwise_finished_goods(self, args, item):
+ qty = flt(self.fg_completed_qty)
+ filters = {
+ "reference_name": self.pro_doc.name,
+ "reference_doctype": self.pro_doc.doctype,
+ "qty_to_produce": (">", 0)
+ }
+
+ fields = ["qty_to_produce as qty", "produced_qty", "name"]
+
+ for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
+ batch_qty = flt(row.qty) - flt(row.produced_qty)
+ if not batch_qty:
+ continue
+
+ if qty <=0:
+ break
+
+ fg_qty = batch_qty
+ if batch_qty >= qty:
+ fg_qty = qty
+
+ qty -= batch_qty
+ args["qty"] = fg_qty
+ args["batch_no"] = row.name
+
+ self.add_finisged_goods(args, item)
+
+ def add_finisged_goods(self, args, item):
self.add_to_stock_entry_detail({
- item.name: {
- "to_warehouse": to_warehouse,
- "from_warehouse": "",
- "qty": self.fg_completed_qty,
- "item_name": item.item_name,
- "description": item.description,
- "stock_uom": item.stock_uom,
- "expense_account": item.get("expense_account"),
- "cost_center": item.get("buying_cost_center"),
- "is_finished_item": 1
- }
+ item.name: args
}, bom_no = self.bom_no)
def get_bom_raw_materials(self, qty):
@@ -1294,7 +1331,8 @@ class StockEntry(StockController):
item_dict[item]["qty"] = 0
# delete items with 0 qty
- for item in item_dict.keys():
+ list_of_items = item_dict.keys()
+ for item in list_of_items:
if not item_dict[item]["qty"]:
del item_dict[item]
@@ -1347,7 +1385,7 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item",
- "expense_account", "description", "item_name"]:
+ "expense_account", "description", "item_name", "serial_no", "batch_no"]:
if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field))
@@ -1400,33 +1438,26 @@ class StockEntry(StockController):
.format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self):
- #Get PO Supplied Items Details
- item_wh = frappe._dict(frappe.db.sql("""
- select rm_item_code, reserve_warehouse
- from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
- where po.name = poitemsup.parent
- and po.name = %s""", self.purchase_order))
+ if (self.purchase_order and
+ (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
- #Update Supplied Qty in PO Supplied Items
+ #Get PO Supplied Items Details
+ item_wh = frappe._dict(frappe.db.sql("""
+ select rm_item_code, reserve_warehouse
+ from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
+ where po.name = poitemsup.parent
+ and po.name = %s""", self.purchase_order))
- frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos
- SET
- pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
- FROM
- `tabStock Entry Detail` sed, `tabStock Entry` se
- WHERE
- 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})
+ supplied_items = get_supplied_items(self.purchase_order)
+ for name, item in supplied_items.items():
+ frappe.db.set_value('Purchase Order Item Supplied', name, item)
- #Update reserved sub contracted quantity in bin based on Supplied Item Details and
- for d in self.get("items"):
- item_code = d.get('original_item') or d.get('item_code')
- reserve_warehouse = item_wh.get(item_code)
- stock_bin = get_bin(item_code, reserve_warehouse)
- stock_bin.update_reserved_qty_for_sub_contracting()
+ #Update reserved sub contracted quantity in bin based on Supplied Item Details and
+ for d in self.get("items"):
+ item_code = d.get('original_item') or d.get('item_code')
+ reserve_warehouse = item_wh.get(item_code)
+ stock_bin = get_bin(item_code, reserve_warehouse)
+ stock_bin.update_reserved_qty_for_sub_contracting()
def update_so_in_serial_number(self):
so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"])
@@ -1480,7 +1511,7 @@ class StockEntry(StockController):
cond += """ WHEN (parent = %s and name = %s) THEN %s
""" %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
- if cond and stock_entries_child_list:
+ if stock_entries_child_list:
frappe.db.sql(""" UPDATE `tabStock Entry Detail`
SET
transferred_qty = CASE {cond} END
@@ -1531,6 +1562,36 @@ class StockEntry(StockController):
material_requests.append(material_request)
frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
+ def set_serial_no_batch_for_finished_good(self):
+ args = {}
+ if self.pro_doc.serial_no:
+ self.get_serial_nos_for_fg(args)
+
+ for row in self.items:
+ if row.is_finished_item and row.item_code == self.pro_doc.production_item:
+ if args.get("serial_no"):
+ row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
+
+ def get_serial_nos_for_fg(self, args):
+ fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
+ "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
+
+ filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
+ ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
+
+ stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
+
+ if self.pro_doc.serial_no:
+ args["serial_no"] = self.get_available_serial_nos(stock_entries)
+
+ def get_available_serial_nos(self, stock_entries):
+ used_serial_nos = []
+ for row in stock_entries:
+ if row.serial_no:
+ used_serial_nos.extend(get_serial_nos(row.serial_no))
+
+ return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):
@@ -1642,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
+ if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
+ 'add_corrective_operation_cost_in_finished_good_valuation')):
+ operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
+
return operating_cost_per_unit
def get_used_alternative_items(purchase_order=None, work_order=None):
@@ -1751,3 +1816,30 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None):
format(max_retain_qty, batch_no, item_code), alert=True)
sample_quantity = qty_diff
return sample_quantity
+
+def get_supplied_items(purchase_order):
+ fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`',
+ '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`']
+
+ filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]]
+
+ supplied_item_details = {}
+ for row in frappe.get_all('Stock Entry', fields = fields, filters = filters):
+ if not row.po_detail:
+ continue
+
+ key = row.po_detail
+ if key not in supplied_item_details:
+ supplied_item_details.setdefault(key,
+ frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0}))
+
+ supplied_item = supplied_item_details[key]
+
+ if row.is_return:
+ supplied_item.returned_qty += row.transfer_qty
+ else:
+ supplied_item.supplied_qty += row.transfer_qty
+
+ supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
+
+ return supplied_item_details
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 864ff488b22..a1782839048 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -18,6 +18,7 @@
"col_break2",
"is_finished_item",
"is_scrap_item",
+ "quality_inspection",
"subcontracted_item",
"section_break_8",
"description",
@@ -69,7 +70,6 @@
"putaway_rule",
"column_break_51",
"reference_purchase_receipt",
- "quality_inspection",
"job_card_item"
],
"fields": [
@@ -548,7 +548,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-11 13:47:50.158754",
+ "modified": "2021-04-22 20:08:23.799715",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
index 0f2b55ec342..eee38be0278 100644
--- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
+++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
@@ -6,7 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "purpose"
+ "purpose",
+ "add_to_transit"
],
"fields": [
{
@@ -18,10 +19,17 @@
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"reqd": 1,
"set_only_once": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.purpose == 'Material Transfer'",
+ "fieldname": "add_to_transit",
+ "fieldtype": "Check",
+ "label": "Add to Transit"
}
],
"links": [],
- "modified": "2020-08-10 23:24:37.160817",
+ "modified": "2021-05-21 11:27:01.144110",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Type",
diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
index a4116aba2c2..1069ec8713e 100644
--- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
+++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
@@ -7,4 +7,6 @@ from __future__ import unicode_literals
from frappe.model.document import Document
class StockEntryType(Document):
- pass
+ def validate(self):
+ if self.add_to_transit and self.purpose != 'Material Transfer':
+ self.add_to_transit = 0
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index b0e7440e6cc..0febcb68910 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, date_diff
+from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, cint
from frappe.model.document import Document
from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -108,17 +108,18 @@ class StockLedgerEntry(Document):
self.stock_uom = item_det.stock_uom
def check_stock_frozen_date(self):
- stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or ''
- if stock_frozen_upto:
- stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
- if getdate(self.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in frappe.get_roles():
- frappe.throw(_("Stock transactions before {0} are frozen").format(formatdate(stock_frozen_upto)), StockFreezeError)
+ stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
- stock_frozen_upto_days = int(frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto_days') or 0)
+ if stock_settings.stock_frozen_upto:
+ if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
+ and stock_settings.stock_auth_role not in frappe.get_roles()):
+ frappe.throw(_("Stock transactions before {0} are frozen")
+ .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError)
+
+ stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days)
if stock_frozen_upto_days:
- stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today())
- if older_than_x_days_ago and not stock_auth_role in frappe.get_roles():
+ if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles():
frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError)
def scrub_posting_time(self):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 3296f5ba4ae..ba31ad7b06b 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -15,10 +15,12 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
+from frappe.core.page.permission_manager.permission_manager import reset
class TestStockLedgerEntry(unittest.TestCase):
def setUp(self):
items = create_items()
+ reset('Stock Entry')
# delete SLE and BINs for all items
frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
@@ -314,10 +316,11 @@ class TestStockLedgerEntry(unittest.TestCase):
# Set User with Stock User role but not Stock Manager
try:
user = frappe.get_doc("User", "test@example.com")
- frappe.set_user(user.name)
user.add_roles("Stock User")
user.remove_roles("Stock Manager")
+ frappe.set_user(user.name)
+
stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
posting_date=add_days(today(), -1), do_not_submit=True)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index ac4ed5e75d9..a01db80da4a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", {
},
get_items: function(frm) {
- frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
+ let fields = [{
+ label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
"get_query": function() {
return {
"filters": {
"company": frm.doc.company,
}
- }
- }},
- function(data) {
- frappe.call({
- method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
- args: {
- warehouse: data.warehouse,
- posting_date: frm.doc.posting_date,
- posting_time: frm.doc.posting_time,
- company:frm.doc.company
- },
- callback: function(r) {
- var items = [];
- frm.clear_table("items");
- for(var i=0; i< r.message.length; i++) {
- var d = frm.add_child("items");
- $.extend(d, r.message[i]);
- if(!d.qty) d.qty = null;
- if(!d.valuation_rate) d.valuation_rate = null;
- }
- frm.refresh_field("items");
- }
- });
+ };
}
- , __("Get Items"), __("Update"));
+ }, {
+ label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
+ "get_query": function() {
+ return {
+ "filters": {
+ "disabled": 0,
+ }
+ };
+ }
+ }];
+
+ frappe.prompt(fields, function(data) {
+ frappe.call({
+ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
+ args: {
+ warehouse: data.warehouse,
+ posting_date: frm.doc.posting_date,
+ posting_time: frm.doc.posting_time,
+ company: frm.doc.company,
+ item_code: data.item_code
+ },
+ callback: function(r) {
+ frm.clear_table("items");
+ for (var i=0; i
100:
+ msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"))
+ self.queue_action('cancel', timeout=2000)
+ else:
+ self._cancel()
+
@frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company):
+def get_items(warehouse, posting_date, posting_time, company, item_code=None):
+ items = [frappe._dict({
+ 'item_code': item_code,
+ 'warehouse': warehouse
+ })]
+
+ if not item_code:
+ items = get_items_for_stock_reco(warehouse, company)
+
+ res = []
+ itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
+
+ for d in items:
+ if d.item_code in itemwise_batch_data:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse,
+ posting_date, posting_time, with_valuation_rate=True)
+
+ for row in itemwise_batch_data.get(d.item_code):
+ args = get_item_data(row, row.qty, stock_bal[1])
+ res.append(args)
+ else:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
+ with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
+
+ args = get_item_data(d, stock_bal[0], stock_bal[1],
+ stock_bal[2] if cint(d.has_serial_no) else '')
+
+ res.append(args)
+
+ return res
+
+def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql("""
- select i.name, i.item_name, bin.warehouse
+ select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabBin bin, tabItem i
- where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
- and i.has_variants = 0 and i.has_serial_no = 0 and i.has_batch_no = 0
- and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
- """, (lft, rgt))
+ where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
+ and i.has_variants = 0 and exists(
+ select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
+ )
+ """, (lft, rgt), as_dict=1)
items += frappe.db.sql("""
- select i.name, i.item_name, id.default_warehouse
+ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabItem i, `tabItem Default` id
where i.name = id.parent
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
- and i.is_stock_item = 1 and i.has_serial_no = 0 and i.has_batch_no = 0
- and i.has_variants = 0 and i.disabled = 0 and id.company=%s
+ and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
group by i.name
- """, (lft, rgt, company))
+ """, (lft, rgt, company), as_dict=1)
- res = []
- for d in set(items):
- stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
- with_valuation_rate=True)
+ return items
- if frappe.db.get_value("Item", d[0], "disabled") == 0:
- res.append({
- "item_code": d[0],
- "warehouse": d[2],
- "qty": stock_bal[0],
- "item_name": d[1],
- "valuation_rate": stock_bal[1],
- "current_qty": stock_bal[0],
- "current_valuation_rate": stock_bal[1]
- })
+def get_item_data(row, qty, valuation_rate, serial_no=None):
+ return {
+ 'item_code': row.item_code,
+ 'warehouse': row.warehouse,
+ 'qty': qty,
+ 'item_name': row.item_name,
+ 'valuation_rate': valuation_rate,
+ 'current_qty': qty,
+ 'current_valuation_rate': valuation_rate,
+ 'current_serial_no': serial_no,
+ 'serial_no': serial_no,
+ 'batch_no': row.get('batch_no')
+ }
- return res
+def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
+ from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+ itemwise_batch_data = {}
+
+ filters = frappe._dict({
+ 'warehouse': warehouse,
+ 'from_date': posting_date,
+ 'to_date': posting_date,
+ 'company': company
+ })
+
+ if item_code:
+ filters.item_code = item_code
+
+ columns, data = execute(filters)
+
+ for row in data:
+ itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
+ 'item_code': row[0],
+ 'warehouse': warehouse,
+ 'qty': row[8],
+ 'item_name': row[1],
+ 'batch_no': row[4]
+ }))
+
+ return itemwise_batch_data
@frappe.whitelist()
def get_stock_balance_for(item_code, warehouse,
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index 85c7ebe2634..6bbba051f98 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2015-02-17 01:06:05.072764",
"doctype": "DocType",
"document_type": "Other",
@@ -170,6 +171,7 @@
},
{
"default": "0",
+ "depends_on": "allow_zero_valuation_rate",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
@@ -179,7 +181,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-03-23 11:09:44.407157",
+ "modified": "2021-05-21 12:13:33.041266",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
@@ -189,4 +191,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 95478f61f0a..e3981c913e1 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -11,6 +11,7 @@ from frappe.test_runner import make_test_records
import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
+from erpnext.stock.doctype.item.test_item import create_item
test_records = frappe.get_test_records('Warehouse')
@@ -92,6 +93,39 @@ class TestWarehouse(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
+ def test_unlinking_warehouse_from_item_defaults(self):
+ company = "_Test Company"
+
+ warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)]
+ warehouse_ids = []
+ for warehouse in warehouse_names:
+ warehouse_id = create_warehouse(warehouse, company=company)
+ warehouse_ids.append(warehouse_id)
+
+ item_names = [f'_Test Item {i} for Unlinking' for i in range(2)]
+ for item, warehouse in zip(item_names, warehouse_ids):
+ create_item(item, warehouse=warehouse, company=company)
+
+ # Delete warehouses
+ for warehouse in warehouse_ids:
+ frappe.delete_doc("Warehouse", warehouse)
+
+ # Check Item existance
+ for item in item_names:
+ self.assertTrue(
+ bool(frappe.db.exists("Item", item)),
+ f"{item} doesn't exist"
+ )
+
+ item_doc = frappe.get_doc("Item", item)
+ for item_default in item_doc.item_defaults:
+ self.assertNotIn(
+ item_default.default_warehouse,
+ warehouse_ids,
+ f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
+ )
+
+
def create_warehouse(warehouse_name, properties=None, company=None):
if not company:
company = "_Test Company"
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 6c84f168fd4..3abc13907cf 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -3,8 +3,9 @@
from __future__ import unicode_literals
import frappe, erpnext
-from frappe.utils import cint, nowdate
+from frappe.utils import cint, flt
from frappe import throw, _
+from collections import defaultdict
from frappe.utils.nestedset import NestedSet
from erpnext.stock import get_warehouse_account
from frappe.contacts.address_and_contact import load_address_and_contact
@@ -53,6 +54,7 @@ class Warehouse(NestedSet):
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
self.update_nsm_model()
+ self.unlink_from_items()
def check_if_sle_exists(self):
return frappe.db.sql("""select name from `tabStock Ledger Entry`
@@ -137,10 +139,14 @@ class Warehouse(NestedSet):
self.save()
return 1
+ def unlink_from_items(self):
+ frappe.db.sql("""
+ update `tabItem Default`
+ set default_warehouse=NULL
+ where default_warehouse=%s""", self.name)
+
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
- from erpnext.stock.utils import get_stock_value_from_bin
-
if is_root:
parent = ""
@@ -153,13 +159,48 @@ def get_children(doctype, parent=None, company=None, is_root=False):
warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name')
+ company_currency = ''
+ if company:
+ company_currency = frappe.get_cached_value('Company', company, 'default_currency')
+
+ warehouse_wise_value = get_warehouse_wise_stock_value(company)
+
# return warehouses
for wh in warehouses:
- wh["balance"] = get_stock_value_from_bin(warehouse=wh.value)
- if company:
- wh["company_currency"] = frappe.db.get_value('Company', company, 'default_currency')
+ wh["balance"] = warehouse_wise_value.get(wh.value)
+ if company_currency:
+ wh["company_currency"] = company_currency
return warehouses
+def get_warehouse_wise_stock_value(company):
+ warehouses = frappe.get_all('Warehouse',
+ fields = ['name', 'parent_warehouse'], filters = {'company': company})
+ parent_warehouse = {d.name : d.parent_warehouse for d in warehouses}
+
+ filters = {'warehouse': ('in', [data.name for data in warehouses])}
+ bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'],
+ filters = filters, group_by = 'warehouse')
+
+ warehouse_wise_stock_value = defaultdict(float)
+ for row in bin_data:
+ if not row.stock_value:
+ continue
+
+ warehouse_wise_stock_value[row.warehouse] = row.stock_value
+ update_value_in_parent_warehouse(warehouse_wise_stock_value,
+ parent_warehouse, row.warehouse, row.stock_value)
+
+ return warehouse_wise_stock_value
+
+def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value):
+ parent_warehouse = parent_warehouse_dict.get(warehouse)
+ if not parent_warehouse:
+ return
+
+ warehouse_wise_stock_value[parent_warehouse] += flt(stock_value)
+ update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict,
+ parent_warehouse, stock_value)
+
@frappe.whitelist()
def add_node():
from frappe.desk.treeview import make_tree_args
diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js
index 3665c0530f2..407d7d1ccd5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse_tree.js
+++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js
@@ -20,7 +20,7 @@ frappe.treeview_settings['Warehouse'] = {
onrender: function(node) {
if (node.data && node.data.balance!==undefined) {
$(''
- + format_currency(Math.abs(node.data.balance), node.data.company_currency)
+ + format_currency((node.data.balance), node.data.company_currency)
+ '').insertBefore(node.$ul);
}
}
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d1dcdc21c87..ca174a3f63c 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -436,18 +436,37 @@ def get_barcode_data(items_list):
return itemwise_barcode
@frappe.whitelist()
-def get_item_tax_info(company, tax_category, item_codes):
+def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None):
out = {}
- if isinstance(item_codes, string_types):
+
+ if item_tax_templates is None:
+ item_tax_templates = {}
+
+ if item_rates is None:
+ item_rates = {}
+
+ if isinstance(item_codes, (str,)):
item_codes = json.loads(item_codes)
+ if isinstance(item_rates, (str,)):
+ item_rates = json.loads(item_rates)
+
+ if isinstance(item_tax_templates, (str,)):
+ item_tax_templates = json.loads(item_tax_templates)
+
for item_code in item_codes:
- if not item_code or item_code in out:
+ if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]):
continue
- out[item_code] = {}
- item = frappe.get_cached_doc("Item", item_code)
- get_item_tax_template({"company": company, "tax_category": tax_category}, item, out[item_code])
- out[item_code]["item_tax_rate"] = get_item_tax_map(company, out[item_code].get("item_tax_template"), as_json=True)
+
+ out[item_code[1]] = {}
+ item = frappe.get_cached_doc("Item", item_code[0])
+ args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])}
+
+ if item_tax_templates:
+ args.update({"item_tax_template": item_tax_templates.get(item_code[1])})
+
+ get_item_tax_template(args, item, out[item_code[1]])
+ out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True)
return out
@@ -459,9 +478,7 @@ def get_item_tax_template(args, item, out):
}
"""
item_tax_template = args.get("item_tax_template")
-
- if not item_tax_template:
- item_tax_template = _get_item_tax_template(args, item.taxes, out)
+ item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_group = item.item_group
@@ -478,12 +495,13 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
for tax in taxes:
tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company')
- if tax.valid_from and tax_company == args['company']:
+ if (tax.valid_from or tax.maximum_net_rate) and tax_company == args['company']:
# In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date
validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
- if getdate(tax.valid_from) <= getdate(validation_date):
+ if getdate(tax.valid_from) <= getdate(validation_date) \
+ and is_within_valid_range(args, tax):
taxes_with_validity.append(tax)
else:
if tax_company == args['company']:
@@ -502,12 +520,26 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if not taxes_with_validity and (not taxes_with_no_validity):
return None
+ # do not change if already a valid template
+ if args.get('item_tax_template') in {t.item_tax_template for t in taxes}:
+ out["item_tax_template"] = args.get('item_tax_template')
+ return args.get('item_tax_template')
+
for tax in taxes:
if cstr(tax.tax_category) == cstr(args.get("tax_category")):
out["item_tax_template"] = tax.item_tax_template
return tax.item_tax_template
return None
+def is_within_valid_range(args, tax):
+ if not flt(tax.maximum_net_rate):
+ # No range specified, just ignore
+ return True
+ elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate):
+ return True
+
+ return False
+
@frappe.whitelist()
def get_item_tax_map(company, item_tax_template, as_json=True):
item_tax_map = {}
diff --git a/erpnext/patches/v5_0/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
similarity index 100%
rename from erpnext/patches/v5_0/__init__.py
rename to erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
new file mode 100644
index 00000000000..bf11277d9c4
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Balance Qty After Transaction"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldtype: "Link",
+ fieldname: "company",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __('Item Code'),
+ fieldtype: 'Link',
+ fieldname: 'item_code',
+ options: 'Item'
+ },
+ {
+ label: __('Warehouse'),
+ fieldtype: 'Link',
+ fieldname: 'warehouse'
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
new file mode 100644
index 00000000000..a5815bcca49
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-12 16:47:58.717853",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-12 16:48:28.347575",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Balance Qty After Transaction",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Balance Qty After Transaction",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Stock Manager"
+ },
+ {
+ "role": "Purchase User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
new file mode 100644
index 00000000000..cf174c93682
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from six import iteritems
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_data(filters):
+ data = get_stock_ledger_entries(filters)
+ itewise_balance_qty = {}
+
+ for row in data:
+ key = (row.item_code, row.warehouse)
+ itewise_balance_qty.setdefault(key, []).append(row)
+
+ res = validate_data(itewise_balance_qty)
+ return res
+
+def validate_data(itewise_balance_qty):
+ res = []
+ for key, data in iteritems(itewise_balance_qty):
+ row = get_incorrect_data(data)
+ if row:
+ res.append(row)
+ res.append({})
+
+ return res
+
+def get_incorrect_data(data):
+ balance_qty = 0.0
+ for row in data:
+ balance_qty += row.actual_qty
+ if row.voucher_type == "Stock Reconciliation" and not row.batch_no:
+ balance_qty = flt(row.qty_after_transaction)
+
+ row.expected_balance_qty = balance_qty
+ if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5:
+ row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction))
+ return row
+
+def get_stock_ledger_entries(report_filters):
+ filters = {}
+ fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
+ 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
+
+ for field in ['warehouse', 'item_code', 'company']:
+ if report_filters.get(field):
+ filters[field] = report_filters.get(field)
+
+ return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+ order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+ return [{
+ 'label': _('Id'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'name',
+ 'options': 'Stock Ledger Entry',
+ 'width': 120
+ }, {
+ 'label': _('Posting Date'),
+ 'fieldtype': 'Date',
+ 'fieldname': 'posting_date',
+ 'width': 110
+ }, {
+ 'label': _('Voucher Type'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'voucher_type',
+ 'options': 'DocType',
+ 'width': 120
+ }, {
+ 'label': _('Voucher No'),
+ 'fieldtype': 'Dynamic Link',
+ 'fieldname': 'voucher_no',
+ 'options': 'voucher_type',
+ 'width': 120
+ }, {
+ 'label': _('Item Code'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'item_code',
+ 'options': 'Item',
+ 'width': 120
+ }, {
+ 'label': _('Warehouse'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'warehouse',
+ 'options': 'Warehouse',
+ 'width': 120
+ }, {
+ 'label': _('Expected Balance Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'expected_balance_qty',
+ 'width': 170
+ }, {
+ 'label': _('Actual Balance Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'qty_after_transaction',
+ 'width': 150
+ }, {
+ 'label': _('Difference'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'differnce',
+ 'width': 110
+ }]
\ No newline at end of file
diff --git a/erpnext/patches/v5_1/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
similarity index 100%
rename from erpnext/patches/v5_1/__init__.py
rename to erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
new file mode 100644
index 00000000000..c62d48081c2
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Serial No Valuation"] = {
+ "filters": [
+ {
+ label: __('Item Code'),
+ fieldtype: 'Link',
+ fieldname: 'item_code',
+ options: 'Item',
+ get_query: function() {
+ return {
+ filters: {
+ 'has_serial_no': 1
+ }
+ }
+ }
+ },
+ {
+ label: __('From Date'),
+ fieldtype: 'Date',
+ fieldname: 'from_date',
+ reqd: 1,
+ default: frappe.defaults.get_user_default("year_start_date")
+ },
+ {
+ label: __('To Date'),
+ fieldtype: 'Date',
+ fieldname: 'to_date',
+ reqd: 1,
+ default: frappe.defaults.get_user_default("year_end_date")
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
new file mode 100644
index 00000000000..cc384a5bd03
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-13 13:07:00.767845",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-05-13 13:07:00.767845",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Serial No Valuation",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Serial No Valuation",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Stock Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
new file mode 100644
index 00000000000..e54cf4c66c7
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import copy
+from frappe import _
+from six import iteritems
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_data(filters):
+ data = get_stock_ledger_entries(filters)
+ serial_nos_data = prepare_serial_nos(data)
+ data = get_incorrect_serial_nos(serial_nos_data)
+
+ return data
+
+def prepare_serial_nos(data):
+ serial_no_wise_data = {}
+ for row in data:
+ if not row.serial_nos:
+ continue
+
+ for serial_no in get_serial_nos(row.serial_nos):
+ sle = copy.deepcopy(row)
+ sle.serial_no = serial_no
+ sle.qty = 1 if sle.actual_qty > 0 else -1
+ sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1
+ serial_no_wise_data.setdefault(serial_no, []).append(sle)
+
+ return serial_no_wise_data
+
+def get_incorrect_serial_nos(serial_nos_data):
+ result = []
+
+ total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))})
+
+ for serial_no, data in iteritems(serial_nos_data):
+ total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))})
+
+ if check_incorrect_serial_data(data, total_dict):
+ result.extend(data)
+
+ total_value.qty += total_dict.qty
+ total_value.valuation_rate += total_dict.valuation_rate
+
+ result.append(total_dict)
+ result.append({})
+
+ result.append(total_value)
+
+ return result
+
+def check_incorrect_serial_data(data, total_dict):
+ incorrect_data = False
+ for row in data:
+ total_dict.qty += row.qty
+ total_dict.valuation_rate += row.valuation_rate
+
+ if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0):
+ incorrect_data = True
+
+ return incorrect_data
+
+def get_stock_ledger_entries(report_filters):
+ fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
+ 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
+
+ filters = {'serial_no': ("is", "set")}
+
+ if report_filters.get('item_code'):
+ filters['item_code'] = report_filters.get('item_code')
+
+ if report_filters.get('from_date') and report_filters.get('to_date'):
+ filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')])
+
+ return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+ order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+ return [{
+ 'label': _('Company'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'company',
+ 'options': 'Company',
+ 'width': 120
+ }, {
+ 'label': _('Id'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'name',
+ 'options': 'Stock Ledger Entry',
+ 'width': 120
+ }, {
+ 'label': _('Posting Date'),
+ 'fieldtype': 'Date',
+ 'fieldname': 'posting_date',
+ 'width': 90
+ }, {
+ 'label': _('Posting Time'),
+ 'fieldtype': 'Time',
+ 'fieldname': 'posting_time',
+ 'width': 90
+ }, {
+ 'label': _('Voucher Type'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'voucher_type',
+ 'options': 'DocType',
+ 'width': 100
+ }, {
+ 'label': _('Voucher No'),
+ 'fieldtype': 'Dynamic Link',
+ 'fieldname': 'voucher_no',
+ 'options': 'voucher_type',
+ 'width': 110
+ }, {
+ 'label': _('Item Code'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'item_code',
+ 'options': 'Item',
+ 'width': 120
+ }, {
+ 'label': _('Warehouse'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'warehouse',
+ 'options': 'Warehouse',
+ 'width': 120
+ }, {
+ 'label': _('Serial No'),
+ 'fieldtype': 'Link',
+ 'fieldname': 'serial_no',
+ 'options': 'Serial No',
+ 'width': 100
+ }, {
+ 'label': _('Qty'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'qty',
+ 'width': 80
+ }, {
+ 'label': _('Valuation Rate (In / Out)'),
+ 'fieldtype': 'Currency',
+ 'fieldname': 'valuation_rate',
+ 'width': 110
+ }]
\ No newline at end of file
diff --git a/erpnext/patches/v5_4/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
similarity index 100%
rename from erpnext/patches/v5_4/__init__.py
rename to erpnext/stock/report/incorrect_stock_value_report/__init__.py
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
new file mode 100644
index 00000000000..ff424807e3e
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Stock Value Report"] = {
+ "filters": [
+ {
+ "label": __("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "label": __("Account"),
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "options": "Account",
+ get_query: function() {
+ var company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: {
+ "account_type": "Stock",
+ "company": company
+ }
+ }
+ }
+ },
+ {
+ "label": __("From Date"),
+ "fieldname": "from_date",
+ "fieldtype": "Date"
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
new file mode 100644
index 00000000000..a7e9f203f7b
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-22 15:35:05.148177",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-22 15:35:05.148177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Stock Value Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Stock Value Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
new file mode 100644
index 00000000000..a7243878eb8
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from six import iteritems
+from frappe.utils import add_days, today, getdate
+from erpnext.stock.utils import get_stock_value_on
+from erpnext.accounts.utils import get_stock_and_account_balance
+
+def execute(filters=None):
+ if not erpnext.is_perpetual_inventory_enabled(filters.company):
+ frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
+ .format(filters.company))
+
+ data = get_data(filters)
+ columns = get_columns(filters)
+
+ return columns, data
+
+def get_unsync_date(filters):
+ date = filters.from_date
+ if not date:
+ date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+ date = date[0][0]
+
+ if not date:
+ return
+
+ while getdate(date) < getdate(today()):
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
+ company=filters.company, account = filters.account)
+
+ if abs(account_bal - stock_bal) > 0.1:
+ return date
+
+ date = add_days(date, 1)
+
+def get_data(report_filters):
+ from_date = get_unsync_date(report_filters)
+
+ if not from_date:
+ return []
+
+ result = []
+
+ voucher_wise_dict = {}
+ data = frappe.db.sql('''
+ SELECT
+ name, posting_date, posting_time, voucher_type, voucher_no,
+ stock_value_difference, stock_value, warehouse, item_code
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ posting_date
+ = %s and company = %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', (from_date, report_filters.company), as_dict=1)
+
+ for d in data:
+ voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
+
+ closing_date = add_days(from_date, -1)
+ for key, stock_data in iteritems(voucher_wise_dict):
+ prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
+ for data in stock_data:
+ expected_stock_value = prev_stock_value + data.stock_value_difference
+ if abs(data.stock_value - expected_stock_value) > 0.1:
+ data.difference_value = abs(data.stock_value - expected_stock_value)
+ data.expected_stock_value = expected_stock_value
+ result.append(data)
+
+ return result
+
+def get_columns(filters):
+ return [
+ {
+ "label": _("Stock Ledger ID"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Stock Ledger Entry",
+ "width": "80"
+ },
+ {
+ "label": _("Posting Date"),
+ "fieldname": "posting_date",
+ "fieldtype": "Date"
+ },
+ {
+ "label": _("Posting Time"),
+ "fieldname": "posting_time",
+ "fieldtype": "Time"
+ },
+ {
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Voucher No"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": "110"
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": "110"
+ },
+ {
+ "label": _("Expected Stock Value"),
+ "fieldname": "expected_stock_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ },
+ {
+ "label": _("Stock Value"),
+ "fieldname": "stock_value",
+ "fieldtype": "Currency",
+ "width": "120"
+ },
+ {
+ "label": _("Difference Value"),
+ "fieldname": "difference_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py
index 5296211fae8..db7498bb215 100644
--- a/erpnext/stock/report/item_price_stock/item_price_stock.py
+++ b/erpnext/stock/report/item_price_stock/item_price_stock.py
@@ -89,7 +89,7 @@ def get_item_price_qty_data(filters):
{conditions}"""
.format(conditions=conditions), filters, as_dict=1)
- price_list_names = list(set([item.price_list_name for item in item_results]))
+ price_list_names = list(set(item.price_list_name for item in item_results))
buying_price_map = get_price_map(price_list_names, buying=1)
selling_price_map = get_price_map(price_list_names, selling=1)
diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py
index e8449cc33ec..d8563d79271 100644
--- a/erpnext/stock/report/item_variant_details/item_variant_details.py
+++ b/erpnext/stock/report/item_variant_details/item_variant_details.py
@@ -14,47 +14,58 @@ def get_data(item):
if not item:
return []
item_dicts = []
- variants = None
- variant_results = frappe.db.sql("""select name from `tabItem`
- where variant_of = %s""", item, as_dict=1)
+ variant_results = frappe.db.get_all(
+ "Item",
+ fields=["name"],
+ filters={
+ "variant_of": ["=", item],
+ "disabled": 0
+ }
+ )
+
if not variant_results:
- frappe.msgprint(_("There isn't any item variant for the selected item"))
+ frappe.msgprint(_("There aren't any item variants for the selected item"))
return []
else:
- variants = ", ".join([frappe.db.escape(variant['name']) for variant in variant_results])
+ variant_list = [variant['name'] for variant in variant_results]
- order_count_map = get_open_sales_orders_map(variants)
- stock_details_map = get_stock_details_map(variants)
- buying_price_map = get_buying_price_map(variants)
- selling_price_map = get_selling_price_map(variants)
- attr_val_map = get_attribute_values_map(variants)
+ order_count_map = get_open_sales_orders_count(variant_list)
+ stock_details_map = get_stock_details_map(variant_list)
+ buying_price_map = get_buying_price_map(variant_list)
+ selling_price_map = get_selling_price_map(variant_list)
+ attr_val_map = get_attribute_values_map(variant_list)
- attribute_list = [d[0] for d in frappe.db.sql("""select attribute
- from `tabItem Variant Attribute`
- where parent in ({variants}) group by attribute""".format(variants=variants))]
+ attributes = frappe.db.get_all(
+ "Item Variant Attribute",
+ fields=["attribute"],
+ filters={
+ "parent": ["in", variant_list]
+ },
+ group_by="attribute"
+ )
+ attribute_list = [row.get("attribute") for row in attributes]
# Prepare dicts
variant_dicts = [{"variant_name": d['name']} for d in variant_results]
for item_dict in variant_dicts:
- name = item_dict["variant_name"]
+ name = item_dict.get("variant_name")
- for d in attribute_list:
- attr_dict = attr_val_map[name]
- if attr_dict and attr_dict.get(d):
- item_dict[d] = attr_val_map[name][d]
+ for attribute in attribute_list:
+ attr_dict = attr_val_map.get(name)
+ if attr_dict and attr_dict.get(attribute):
+ item_dict[frappe.scrub(attribute)] = attr_val_map.get(name).get(attribute)
- item_dict["Open Orders"] = order_count_map.get(name) or 0
+ item_dict["open_orders"] = order_count_map.get(name) or 0
if stock_details_map.get(name):
- item_dict["Inventory"] = stock_details_map.get(name)["Inventory"] or 0
- item_dict["In Production"] = stock_details_map.get(name)["In Production"] or 0
- item_dict["Available Selling"] = stock_details_map.get(name)["Available Selling"] or 0
+ item_dict["current_stock"] = stock_details_map.get(name)["Inventory"] or 0
+ item_dict["in_production"] = stock_details_map.get(name)["In Production"] or 0
else:
- item_dict["Inventory"] = item_dict["In Production"] = item_dict["Available Selling"] = 0
+ item_dict["current_stock"] = item_dict["in_production"] = 0
- item_dict["Avg. Buying Price List Rate"] = buying_price_map.get(name) or 0
- item_dict["Avg. Selling Price List Rate"] = selling_price_map.get(name) or 0
+ item_dict["avg_buying_price_list_rate"] = buying_price_map.get(name) or 0
+ item_dict["avg_selling_price_list_rate"] = selling_price_map.get(name) or 0
item_dicts.append(item_dict)
@@ -71,117 +82,158 @@ def get_columns(item):
item_doc = frappe.get_doc("Item", item)
- for d in item_doc.attributes:
- columns.append(d.attribute + ":Data:100")
+ for entry in item_doc.attributes:
+ columns.append({
+ "fieldname": frappe.scrub(entry.attribute),
+ "label": entry.attribute,
+ "fieldtype": "Data",
+ "width": 100
+ })
- columns += [_("Avg. Buying Price List Rate") + ":Currency:110", _("Avg. Selling Price List Rate") + ":Currency:110",
- _("Inventory") + ":Float:100", _("In Production") + ":Float:100",
- _("Open Orders") + ":Float:100", _("Available Selling") + ":Float:100"
+ additional_columns = [
+ {
+ "fieldname": "avg_buying_price_list_rate",
+ "label": _("Avg. Buying Price List Rate"),
+ "fieldtype": "Currency",
+ "width": 150
+ },
+ {
+ "fieldname": "avg_selling_price_list_rate",
+ "label": _("Avg. Selling Price List Rate"),
+ "fieldtype": "Currency",
+ "width": 150
+ },
+ {
+ "fieldname": "current_stock",
+ "label": _("Current Stock"),
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "fieldname": "in_production",
+ "label": _("In Production"),
+ "fieldtype": "Float",
+ "width": 150
+ },
+ {
+ "fieldname": "open_orders",
+ "label": _("Open Sales Orders"),
+ "fieldtype": "Float",
+ "width": 150
+ }
]
+ columns.extend(additional_columns)
return columns
-def get_open_sales_orders_map(variants):
- open_sales_orders = frappe.db.sql("""
- select
- count(*) as count,
- item_code
- from
- `tabSales Order Item`
- where
- docstatus = 1 and
- qty > ifnull(delivered_qty, 0) and
- item_code in ({variants})
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_open_sales_orders_count(variants_list):
+ open_sales_orders = frappe.db.get_list(
+ "Sales Order",
+ fields=[
+ "name",
+ "`tabSales Order Item`.item_code"
+ ],
+ filters=[
+ ["Sales Order", "docstatus", "=", 1],
+ ["Sales Order Item", "item_code", "in", variants_list]
+ ],
+ distinct=1
+ )
order_count_map = {}
- for d in open_sales_orders:
- order_count_map[d["item_code"]] = d["count"]
+ for row in open_sales_orders:
+ item_code = row.get("item_code")
+ if order_count_map.get(item_code) is None:
+ order_count_map[item_code] = 1
+ else:
+ order_count_map[item_code] += 1
return order_count_map
-def get_stock_details_map(variants):
- stock_details = frappe.db.sql("""
- select
- sum(planned_qty) as planned_qty,
- sum(actual_qty) as actual_qty,
- sum(projected_qty) as projected_qty,
- item_code
- from
- `tabBin`
- where
- item_code in ({variants})
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_stock_details_map(variant_list):
+ stock_details = frappe.db.get_all(
+ "Bin",
+ fields=[
+ "sum(planned_qty) as planned_qty",
+ "sum(actual_qty) as actual_qty",
+ "sum(projected_qty) as projected_qty",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list]
+ },
+ group_by="item_code"
+ )
stock_details_map = {}
- for d in stock_details:
- name = d["item_code"]
+ for row in stock_details:
+ name = row.get("item_code")
stock_details_map[name] = {
- "Inventory" :d["actual_qty"],
- "In Production" :d["planned_qty"],
- "Available Selling" :d["projected_qty"]
+ "Inventory": row.get("actual_qty"),
+ "In Production": row.get("planned_qty")
}
return stock_details_map
-def get_buying_price_map(variants):
- buying = frappe.db.sql("""
- select
- avg(price_list_rate) as avg_rate,
- item_code
- from
- `tabItem Price`
- where
- item_code in ({variants}) and buying=1
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_buying_price_map(variant_list):
+ buying = frappe.db.get_all(
+ "Item Price",
+ fields=[
+ "avg(price_list_rate) as avg_rate",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list],
+ "buying": 1
+ },
+ group_by="item_code"
+ )
buying_price_map = {}
- for d in buying:
- buying_price_map[d["item_code"]] = d["avg_rate"]
+ for row in buying:
+ buying_price_map[row.get("item_code")] = row.get("avg_rate")
return buying_price_map
-def get_selling_price_map(variants):
- selling = frappe.db.sql("""
- select
- avg(price_list_rate) as avg_rate,
- item_code
- from
- `tabItem Price`
- where
- item_code in ({variants}) and selling=1
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_selling_price_map(variant_list):
+ selling = frappe.db.get_all(
+ "Item Price",
+ fields=[
+ "avg(price_list_rate) as avg_rate",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list],
+ "selling": 1
+ },
+ group_by="item_code"
+ )
selling_price_map = {}
- for d in selling:
- selling_price_map[d["item_code"]] = d["avg_rate"]
+ for row in selling:
+ selling_price_map[row.get("item_code")] = row.get("avg_rate")
return selling_price_map
-def get_attribute_values_map(variants):
- list_attr = frappe.db.sql("""
- select
- attribute, attribute_value, parent
- from
- `tabItem Variant Attribute`
- where
- parent in ({variants})
- """.format(variants=variants), as_dict=1)
+def get_attribute_values_map(variant_list):
+ attribute_list = frappe.db.get_all(
+ "Item Variant Attribute",
+ fields=[
+ "attribute",
+ "attribute_value",
+ "parent"
+ ],
+ filters={
+ "parent": ["in", variant_list]
+ }
+ )
attr_val_map = {}
- for d in list_attr:
- name = d["parent"]
+ for row in attribute_list:
+ name = row.get("parent")
if not attr_val_map.get(name):
attr_val_map[name] = {}
- attr_val_map[name][d["attribute"]] = d["attribute_value"]
+ attr_val_map[name][row.get("attribute")] = row.get("attribute_value")
return attr_val_map
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index 2f70523264a..2e13aa0b040 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -66,7 +66,7 @@ def get_consumed_items(condition):
purpose is NULL
or purpose not in ({})
)
- """.format(', '.join([f"'{p}'" for p in purpose_to_exclude]))
+ """.format(', '.join(f"'{p}'" for p in purpose_to_exclude))
condition = condition.replace("posting_date", "sle.posting_date")
consumed_items = frappe.db.sql("""
diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py
index 276e42ee433..8fffbccab3c 100644
--- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py
+++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py
@@ -141,7 +141,7 @@ def get_stock_ledger_entries(filters, items):
return []
item_conditions_sql = ' and sle.item_code in ({})' \
- .format(', '.join([frappe.db.escape(i) for i in items]))
+ .format(', '.join(frappe.db.escape(i) for i in items))
conditions = get_sle_conditions(filters)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index bbd73e91129..b6a80631892 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -157,7 +157,7 @@ def get_stock_ledger_entries(filters, items):
item_conditions_sql = ''
if items:
item_conditions_sql = ' and sle.item_code in ({})'\
- .format(', '.join([frappe.db.escape(i, percent=False) for i in items]))
+ .format(', '.join(frappe.db.escape(i, percent=False) for i in items))
conditions = get_conditions(filters)
@@ -253,7 +253,7 @@ def get_items(filters):
def get_item_details(items, sle, filters):
item_details = {}
if not items:
- items = list(set([d.item_code for d in sle]))
+ items = list(set(d.item_code for d in sle))
if not items:
return item_details
@@ -291,7 +291,7 @@ def get_item_reorder_details(items):
select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level
from `tabItem Reorder`
where parent in ({0})
- """.format(', '.join([frappe.db.escape(i, percent=False) for i in items])), as_dict=1)
+ """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1)
return dict((d.parent + d.warehouse, d) for d in item_reorder_details)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 36996e96745..8909f217f49 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -115,7 +115,7 @@ def get_stock_ledger_entries(filters, items):
item_conditions_sql = ''
if items:
item_conditions_sql = 'and sle.item_code in ({})'\
- .format(', '.join([frappe.db.escape(i) for i in items]))
+ .format(', '.join(frappe.db.escape(i) for i in items))
sl_entries = frappe.db.sql("""
SELECT
@@ -169,7 +169,7 @@ def get_items(filters):
def get_item_details(items, sl_entries, include_uom):
item_details = {}
if not items:
- items = list(set([d.item_code for d in sl_entries]))
+ items = list(set(d.item_code for d in sl_entries))
if not items:
return item_details
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 8ba1f1ca5c7..8917bfeae4f 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -194,9 +194,6 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
serial_nos = frappe.db.sql("""select count(name) from `tabSerial No`
where item_code=%s and warehouse=%s and docstatus < 2""", (d[0], d[1]))
- if serial_nos and flt(serial_nos[0][0]) != flt(d[2]):
- print(d[0], d[1], d[2], serial_nos[0][0])
-
sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s and is_cancelled = 0
order by posting_date desc limit 1""", (d[0], d[1]))
@@ -230,7 +227,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
})
update_bin(args)
-
+
create_repost_item_valuation_entry({
"item_code": d[0],
"warehouse": d[1],
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9729987d2d3..9fe89c3fa59 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions')
# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
+ from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
from erpnext.stock.utils import update_bin
@@ -30,6 +31,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
+ args = get_args_for_future_sle(sl_entries[0])
+ future_sle_exists(args, sl_entries)
+
for sle in sl_entries:
if sle.serial_no:
validate_serial_no(sle)
@@ -53,6 +57,14 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+def get_args_for_future_sle(row):
+ return frappe._dict({
+ 'voucher_type': row.get('voucher_type'),
+ 'voucher_no': row.get('voucher_no'),
+ 'posting_date': row.get('posting_date'),
+ 'posting_time': row.get('posting_time')
+ })
+
def validate_serial_no(sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for sn in get_serial_nos(sle.serial_no):
@@ -232,7 +244,8 @@ class update_entries_after(object):
and is_cancelled = 0
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
- limit 1""", args, as_dict=1)
+ limit 1
+ for update""", args, as_dict=1)
return sle[0] if sle else frappe._dict()
@@ -471,7 +484,7 @@ class update_entries_after(object):
frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
- if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
+ if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False)
for d in (doc.items + doc.supplied_items):
@@ -520,7 +533,7 @@ class update_entries_after(object):
fields=["purchase_rate", "name", "company"],
filters = {'name': ('in', serial_nos)})
- incoming_values = sum([flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company])
+ incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company)
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company]
@@ -623,7 +636,7 @@ class update_entries_after(object):
break
# If no entry found with outgoing rate, collapse stack
- if index == None:
+ if index is None: # nosemgrep
new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 034d3ebbb54..8a6a3a3e4a0 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -177,7 +177,7 @@ def get_bin(item_code, warehouse):
return bin_obj
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
- is_stock_item = frappe.db.get_value('Item', args.get("item_code"), 'is_stock_item')
+ is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
bin = get_bin(args.get("item_code"), args.get("warehouse"))
bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 3221dc4365c..529ce8eb61e 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -15,6 +15,7 @@
"hide_custom": 0,
"icon": "stock",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Stock",
"links": [
@@ -653,9 +654,44 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Data Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Serial No Qty and Valuation",
+ "link_to": "Incorrect Serial No Valuation",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Incorrect Balance Qty After Transaction",
+ "link_to": "Incorrect Balance Qty After Transaction",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock and Account Value Comparison",
+ "link_to": "Stock and Account Value Comparison",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2020-12-01 13:38:36.282890",
+ "modified": "2021-05-13 13:10:24.914983",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index a43381c5c6b..14712f89feb 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -119,7 +119,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Open\nReplied\nHold\nResolved\nClosed",
+ "options": "Open\nReplied\nOn Hold\nResolved\nClosed",
"search_index": 1
},
{
@@ -166,7 +166,7 @@
"options": "Service Level Agreement"
},
{
- "depends_on": "eval: doc.status != 'Replied';",
+ "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "response_by",
"fieldtype": "Datetime",
"label": "Response By",
@@ -180,7 +180,7 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.status != 'Replied';",
+ "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "resolution_by",
"fieldtype": "Datetime",
"label": "Resolution By",
@@ -410,7 +410,7 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2020-08-11 18:49:07.574769",
+ "modified": "2021-06-10 03:22:27.098898",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index b068363f061..9c69deb6a48 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -29,6 +29,9 @@ class Issue(Document):
self.update_status()
self.set_lead_contact(self.raised_by)
+ if not self.service_level_agreement:
+ self.reset_sla_fields()
+
def on_update(self):
# Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal:
@@ -54,6 +57,13 @@ class Issue(Document):
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company")
+ def reset_sla_fields(self):
+ self.agreement_status = ""
+ self.response_by = ""
+ self.resolution_by = ""
+ self.response_by_variance = 0
+ self.resolution_by_variance = 0
+
def update_status(self):
status = frappe.db.get_value("Issue", self.name, "status")
if self.status != "Open" and status == "Open" and not self.first_responded_on:
@@ -511,4 +521,4 @@ def get_time_in_timedelta(time):
Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)
"""
import datetime
- return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
\ No newline at end of file
+ return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py
index a3428a25af4..a20e7a801b0 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.py
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py
@@ -29,7 +29,7 @@ class WarrantyClaim(TransactionBase):
where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""",
(self.name))
if lst:
- lst1 = ','.join([x[0] for x in lst])
+ lst1 = ','.join(x[0] for x in lst)
frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1))
else:
frappe.db.set(self, 'status', 'Cancelled')
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
index 576e0b76dad..18691fe264f 100644
--- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
@@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = {
get_chart_data: function(_columns, result) {
return {
data: {
- labels: result.map(d => d[0]),
+ labels: result.map(d => d.creation_date),
datasets: [{
name: 'First Response Time',
- values: result.map(d => d[1])
+ values: result.map(d => d.first_response_time)
}]
},
type: "line",
@@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = {
hide_days: 0,
hide_seconds: 0
};
- value = frappe.utils.get_formatted_duration(d, duration_options);
- return value;
+ return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}
diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js
index eb0e06cd08b..a5122d03ad1 100644
--- a/erpnext/support/report/issue_summary/issue_summary.js
+++ b/erpnext/support/report/issue_summary/issue_summary.js
@@ -42,6 +42,7 @@ frappe.query_reports["Issue Summary"] = {
"",
{label: __('Open'), value: 'Open'},
{label: __('Replied'), value: 'Replied'},
+ {label: __('On Hold'), value: 'On Hold'},
{label: __('Resolved'), value: 'Resolved'},
{label: __('Closed'), value: 'Closed'}
]
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index 7861e30d252..bba25b8bed6 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -62,7 +62,7 @@ class IssueSummary(object):
'width': 200
})
- self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
+ self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']
for status in self.statuses:
self.columns.append({
'label': _(status),
@@ -265,6 +265,7 @@ class IssueSummary(object):
labels = []
open_issues = []
replied_issues = []
+ on_hold_issues = []
resolved_issues = []
closed_issues = []
@@ -277,6 +278,7 @@ class IssueSummary(object):
labels.append(entry.get(entity_field))
open_issues.append(entry.get('open'))
replied_issues.append(entry.get('replied'))
+ on_hold_issues.append(entry.get('on_hold'))
resolved_issues.append(entry.get('resolved'))
closed_issues.append(entry.get('closed'))
@@ -292,6 +294,10 @@ class IssueSummary(object):
'name': 'Replied',
'values': replied_issues[:30]
},
+ {
+ 'name': 'On Hold',
+ 'values': on_hold_issues[:30]
+ },
{
'name': 'Resolved',
'values': resolved_issues[:30]
@@ -313,12 +319,14 @@ class IssueSummary(object):
open_issues = 0
replied = 0
+ on_hold = 0
resolved = 0
closed = 0
for entry in self.data:
open_issues += entry.get('open')
replied += entry.get('replied')
+ on_hold += entry.get('on_hold')
resolved += entry.get('resolved')
closed += entry.get('closed')
@@ -335,6 +343,12 @@ class IssueSummary(object):
'label': _('Replied'),
'datatype': 'Int',
},
+ {
+ 'value': on_hold,
+ 'indicator': 'Grey',
+ 'label': _('On Hold'),
+ 'datatype': 'Int',
+ },
{
'value': resolved,
'indicator': 'Green',
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index 84a9430956e..4482bc10cf7 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -99,6 +99,7 @@ frappe.ready(() => {
fieldname: 'country',
fieldtype: 'Link',
options: 'Country',
+ only_select: true,
reqd: 1
},
{
diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html
index 383413103e1..3cfb8d8440c 100644
--- a/erpnext/templates/includes/transaction_row.html
+++ b/erpnext/templates/includes/transaction_row.html
@@ -13,9 +13,11 @@
{{ doc.items_preview }}