diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7fc05ddc510..ac2b7497d23 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -663,4 +663,5 @@ erpnext.patches.v12_0.move_bank_account_swift_number_to_bank erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.recalculate_requested_qty_in_bin -erpnext.patches.v12_0.update_healthcare_refactored_changes \ No newline at end of file +erpnext.patches.v12_0.update_healthcare_refactored_changes +erpnext.patches.v12_0.set_total_batch_quantity diff --git a/erpnext/patches/v10_0/update_address_template_for_india.py b/erpnext/patches/v10_0/update_address_template_for_india.py index 145ed452e9c..1ddca937609 100644 --- a/erpnext/patches/v10_0/update_address_template_for_india.py +++ b/erpnext/patches/v10_0/update_address_template_for_india.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals import frappe -from erpnext.regional.india.setup import update_address_template +from erpnext.regional.address_template.setup import set_up_address_templates def execute(): if frappe.db.get_value('Company', {'country': 'India'}, 'name'): address_template = frappe.db.get_value('Address Template', 'India', 'template') if not address_template or "gstin" not in address_template: - update_address_template() + set_up_address_templates(default_country='India') diff --git a/erpnext/patches/v12_0/set_total_batch_quantity.py b/erpnext/patches/v12_0/set_total_batch_quantity.py new file mode 100644 index 00000000000..d373275c253 --- /dev/null +++ b/erpnext/patches/v12_0/set_total_batch_quantity.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + frappe.reload_doc("stock", "doctype", "batch") + + for batch in frappe.get_all("Batch", fields=["name", "batch_id"]): + batch_qty = frappe.db.get_value("Stock Ledger Entry", + {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": "No"}, + "sum(actual_qty)") or 0.0 + frappe.db.set_value("Batch", batch.name, "batch_qty", batch_qty, update_modified=False) diff --git a/erpnext/patches/v8_1/gst_fixes.py b/erpnext/patches/v8_1/gst_fixes.py index 22fa53ba373..34255eb0a42 100644 --- a/erpnext/patches/v8_1/gst_fixes.py +++ b/erpnext/patches/v8_1/gst_fixes.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from erpnext.regional.india.setup import update_address_template +from erpnext.regional.address_template.setup import set_up_address_templates + def execute(): company = frappe.get_all('Company', filters = {'country': 'India'}) @@ -10,9 +11,10 @@ def execute(): update_existing_custom_fields() add_custom_fields() - update_address_template() + set_up_address_templates(default_country='India') frappe.reload_doc("regional", "print_format", "gst_tax_invoice") + def update_existing_custom_fields(): frappe.db.sql("""update `tabCustom Field` set label = 'HSN/SAC' where fieldname='gst_hsn_code' and label='GST HSN Code' @@ -34,6 +36,7 @@ def update_existing_custom_fields(): where fieldname='gst_hsn_code' and dt in ('Sales Invoice Item', 'Purchase Invoice Item') """) + def add_custom_fields(): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Data', options='item_code.gst_hsn_code', insert_after='description') diff --git a/erpnext/regional/address_template/README.md b/erpnext/regional/address_template/README.md new file mode 100644 index 00000000000..991573448e1 --- /dev/null +++ b/erpnext/regional/address_template/README.md @@ -0,0 +1,18 @@ +To add an **Address Template** for your country, place a new file in this directory: + + * File name: `your_country.html` (lower case with underscores) + * File content: a [Jinja Template](http://jinja.pocoo.org/docs/templates/). + +All the fields of **Address** (including Custom Fields, if any) will be available to the template. Example: + +```jinja +{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%} +{{ city }}
+{% if state %}{{ state }}
{% endif -%} +{% if pincode %} PIN: {{ pincode }}
{% endif -%} +{{ country }}
+{% if phone %}Phone: {{ phone }}
{% endif -%} +{% if fax %}Fax: {{ fax }}
{% endif -%} +{% if email_id %}Email: {{ email_id }}
{% endif -%} +``` diff --git a/erpnext/regional/address_template/__init__.py b/erpnext/regional/address_template/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/address_template/setup.py b/erpnext/regional/address_template/setup.py new file mode 100644 index 00000000000..9f318de3451 --- /dev/null +++ b/erpnext/regional/address_template/setup.py @@ -0,0 +1,53 @@ +"""Import Address Templates from ./templates directory.""" +import os +import frappe + +def set_up_address_templates(default_country=None): + for country, html in get_address_templates(): + is_default = 1 if country == default_country else 0 + update_address_template(country, html, is_default) + +def get_address_templates(): + """ + Return country and path for all HTML files in this directory. + + Returns a list of dicts. + """ + def country(file_name): + """Convert 'united_states.html' to 'United States'.""" + suffix_pos = file_name.find(".html") + country_snake_case = file_name[:suffix_pos] + country_title_case = " ".join(country_snake_case.split("_")).title() + return country_title_case + + def get_file_content(file_name): + """Convert 'united_states.html' to '/path/to/united_states.html'.""" + full_path = os.path.join(template_dir, file_name) + with open(full_path, "r") as f: + content = f.read() + return content + + dir_name = os.path.dirname(__file__) + template_dir = os.path.join(dir_name, "templates") + file_names = os.listdir(template_dir) + html_files = [file for file in file_names if file.endswith(".html")] + + return [(country(file_name), get_file_content(file_name)) for file_name in html_files] + + +def update_address_template(country, html, is_default=0): + """Update existing Address Template or create a new one.""" + if not frappe.db.exists("Country", country): + frappe.log_error("Country {} for regional Address Template does not exist.".format(country)) + return + + if frappe.db.exists("Address Template", country): + frappe.db.set_value("Address Template", country, "template", html) + frappe.db.set_value("Address Template", country, "is_default", is_default) + else: + frappe.get_doc(dict( + doctype="Address Template", + country=country, + is_default=is_default, + template=html + )).insert() diff --git a/erpnext/regional/germany/address_template.html b/erpnext/regional/address_template/templates/germany.html similarity index 100% rename from erpnext/regional/germany/address_template.html rename to erpnext/regional/address_template/templates/germany.html diff --git a/erpnext/regional/india/address_template.html b/erpnext/regional/address_template/templates/india.html similarity index 89% rename from erpnext/regional/india/address_template.html rename to erpnext/regional/address_template/templates/india.html index 55cc9af0d16..ffb9d0547e0 100644 --- a/erpnext/regional/india/address_template.html +++ b/erpnext/regional/address_template/templates/india.html @@ -6,4 +6,4 @@ {% if phone %}Phone: {{ phone }}
{% endif -%} {% if fax %}Fax: {{ fax }}
{% endif -%} {% if email_id %}Email: {{ email_id }}
{% endif -%} -{% if gstin %}GSTIN: {{ gstin }}
{% endif -%} \ No newline at end of file +{% if gstin %}GSTIN: {{ gstin }}
{% endif -%} diff --git a/erpnext/regional/united_states/address_template.html b/erpnext/regional/address_template/templates/united_states.html similarity index 100% rename from erpnext/regional/united_states/address_template.html rename to erpnext/regional/address_template/templates/united_states.html diff --git a/erpnext/regional/address_template/test_regional_address_template.py b/erpnext/regional/address_template/test_regional_address_template.py new file mode 100644 index 00000000000..8a05ea26f45 --- /dev/null +++ b/erpnext/regional/address_template/test_regional_address_template.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals +from unittest import TestCase + +import frappe +from erpnext.regional.address_template.setup import get_address_templates +from erpnext.regional.address_template.setup import update_address_template + +def ensure_country(country): + if frappe.db.exists("Country", country): + return frappe.get_doc("Country", country) + else: + c = frappe.get_doc({ + "doctype": "Country", + "country_name": country + }) + c.insert() + return c + +class TestRegionalAddressTemplate(TestCase): + def test_get_address_templates(self): + """Get the countries and paths from the templates directory.""" + templates = get_address_templates() + self.assertIsInstance(templates, list) + self.assertIsInstance(templates[0], tuple) + + def test_create_address_template(self): + """Create a new Address Template.""" + country = ensure_country("Germany") + update_address_template(country.name, "TEST") + doc = frappe.get_doc("Address Template", country.name) + self.assertEqual(doc.template, "TEST") + + def test_update_address_template(self): + """Update an existing Address Template.""" + country = ensure_country("Germany") + if not frappe.db.exists("Address Template", country.name): + template = frappe.get_doc({ + "doctype": "Address Template", + "country": country.name, + "template": "EXISTING" + }).insert() + + update_address_template(country.name, "NEW") + doc = frappe.get_doc("Address Template", country.name) + self.assertEqual(doc.template, "NEW") diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py index a5471366ca2..d6047e863ce 100644 --- a/erpnext/regional/germany/setup.py +++ b/erpnext/regional/germany/setup.py @@ -3,29 +3,4 @@ import frappe def setup(company=None, patch=True): - if not patch: - update_address_template() - - -def update_address_template(): - """ - Read address template from file. Update existing Address Template or create a - new one. - """ - dir_name = os.path.dirname(__file__) - template_path = os.path.join(dir_name, 'address_template.html') - - with open(template_path, 'r') as template_file: - template_html = template_file.read() - - address_template = frappe.db.get_value('Address Template', 'Germany') - - if address_template: - frappe.db.set_value('Address Template', 'Germany', 'template', template_html) - else: - # make new html template for Germany - frappe.get_doc(dict( - doctype='Address Template', - country='Germany', - template=template_html - )).insert() + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 75f29b8380b..28b1f8ffb8b 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -13,7 +13,6 @@ from frappe.utils import today def setup(company=None, patch=True): setup_company_independent_fixtures() if not patch: - update_address_template() make_fixtures(company) # TODO: for all countries @@ -24,21 +23,6 @@ def setup_company_independent_fixtures(): frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) add_print_formats() -def update_address_template(): - with open(os.path.join(os.path.dirname(__file__), 'address_template.html'), 'r') as f: - html = f.read() - - address_template = frappe.db.get_value('Address Template', 'India') - if address_template: - frappe.db.set_value('Address Template', 'India', 'template', html) - else: - # make new html template for India - frappe.get_doc(dict( - doctype='Address Template', - country='India', - template=html - )).insert() - def add_hsn_sac_codes(): # HSN codes with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index cb82b639ba8..6d344025d26 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -5,12 +5,9 @@ from __future__ import unicode_literals import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - def setup(company=None, patch=True): make_custom_fields() add_print_formats() - update_address_template() - def make_custom_fields(): custom_fields = { @@ -21,22 +18,7 @@ def make_custom_fields(): } create_custom_fields(custom_fields) - def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where name in('IRS 1099 Form') """) - - -def update_address_template(): - html = """{{ address_line1 }}
- {% if address_line2 %}{{ address_line2 }}
{% endif -%} - {{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}
{% endif -%} - {% if country != "United States" %}{{ country|upper }}{% endif -%} - """ - - address_template = frappe.db.get_value('Address Template', 'United States') - if address_template: - frappe.db.set_value('Address Template', 'United States', 'template', html) - else: - frappe.get_doc(dict(doctype='Address Template', country='United States', template=html)).insert() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ebd7b509396..e4986e36b7b 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -8,9 +8,11 @@ import frappe, os, json from frappe import _ from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.utils import cstr, getdate -from erpnext.accounts.doctype.account.account import RootNotEditable from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes +from erpnext.accounts.doctype.account.account import RootNotEditable +from erpnext.regional.address_template.setup import set_up_address_templates + default_lead_sources = ["Existing Customer", "Reference", "Advertisement", "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", "Customer's Vendor", "Campaign", "Walk In"] @@ -30,7 +32,7 @@ def install(country=None): { 'doctype': 'Domain', 'domain': 'Agriculture'}, { 'doctype': 'Domain', 'domain': 'Non Profit'}, - # address template + # ensure at least an empty Address Template exists for this Country {'doctype':"Address Template", "country": country}, # item group @@ -269,12 +271,11 @@ def install(country=None): # Records for the Supplier Scorecard from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records + make_default_records() - make_records(records) - + set_up_address_templates(default_country=country) set_more_defaults() - update_global_search_doctypes() # path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index e6d4e9d8b1b..1eb457734e0 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -1,554 +1,194 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, "allow_import": 1, - "allow_rename": 0, "autoname": "field:batch_id", - "beta": 0, "creation": "2013-03-05 14:50:38", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "sb_disabled", + "disabled", + "sb_batch", + "batch_id", + "item", + "item_name", + "image", + "parent_batch", + "manufacturing_date", + "column_break_3", + "batch_qty", + "stock_uom", + "expiry_date", + "source", + "supplier", + "column_break_9", + "reference_doctype", + "reference_name", + "section_break_7", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { "depends_on": "eval:doc.__islocal", "fieldname": "batch_id", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Batch ID", - "length": 0, "no_copy": 1, "oldfieldname": "batch_id", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "item", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, "in_standard_filter": 1, "label": "Item", - "length": 0, - "no_copy": 0, "oldfieldname": "item", "oldfieldtype": "Link", "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "image" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.parent_batch", "fieldname": "parent_batch", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Parent Batch", - "length": 0, - "no_copy": 0, "options": "Batch", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "manufacturing_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Manufacturing Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Manufacturing Date" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expiry_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Expiry Date", - "length": 0, - "no_copy": 0, "oldfieldname": "expiry_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "source", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Source", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Source" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "supplier", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Supplier", - "length": 0, - "no_copy": 0, "options": "Supplier", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Source Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_name", "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Source Document Name", - "length": 0, - "no_copy": 0, "options": "reference_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "description", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Batch Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" + }, + { + "fieldname": "sb_disabled", + "fieldtype": "Section Break" + }, + { + "fieldname": "sb_batch", + "fieldtype": "Section Break", + "label": "Batch Details" + }, + { + "fetch_from": "item.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "batch_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Batch Quantity", + "read_only": 1 + }, + { + "fetch_from": "item.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Batch UOM", + "options": "UOM", + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-archive", "idx": 1, "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, "max_attachments": 5, - "modified": "2018-08-29 06:28:57.985997", + "modified": "2019-10-03 22:38:45.104056", "modified_by": "Administrator", "module": "Stock", "name": "Batch", "owner": "harshada@webnotestech.com", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Item Manager", "set_user_permissions": 1, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "sort_field": "modified", "sort_order": "DESC", - "title_field": "item", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "batch_id" } \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch_dashboard.py b/erpnext/stock/doctype/batch/batch_dashboard.py new file mode 100644 index 00000000000..eb6a97ecfd8 --- /dev/null +++ b/erpnext/stock/doctype/batch/batch_dashboard.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from frappe import _ + + +def get_data(): + return { + 'fieldname': 'batch_no', + 'transactions': [ + { + 'label': _('Buy'), + 'items': ['Purchase Invoice', 'Purchase Receipt'] + }, + { + 'label': _('Sell'), + 'items': ['Sales Invoice', 'Delivery Note'] + }, + { + 'label': _('Move'), + 'items': ['Stock Entry'] + }, + { + 'label': _('Quality'), + 'items': ['Quality Inspection'] + } + ] + } diff --git a/erpnext/stock/doctype/batch/batch_list.js b/erpnext/stock/doctype/batch/batch_list.js index 7ee81aa7e9f..d4f74c3a217 100644 --- a/erpnext/stock/doctype/batch/batch_list.js +++ b/erpnext/stock/doctype/batch/batch_list.js @@ -1,12 +1,14 @@ frappe.listview_settings['Batch'] = { - add_fields: ["item", "expiry_date"], - get_indicator: function(doc) { - if(doc.expiry_date && frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0) { - return [__("Expired"), "red", "expiry_date,>=,Today"] - } else if(doc.expiry_date) { - return [__("Not Expired"), "green", "expiry_date,<,Today"] + add_fields: ["item", "expiry_date", "batch_qty", "disabled"], + get_indicator: (doc) => { + if (doc.disabled) { + return [__("Disabled"), "darkgrey", "disabled,=,1"]; + } else if (!doc.batch_qty) { + return [__("Empty"), "darkgrey", "batch_qty,=,0|disabled,=,0"]; + } else if (doc.expiry_date && frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0) { + return [__("Expired"), "red", "expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0"] } else { - return ["Not Set", "darkgrey", ""]; - } + return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"]; + }; } }; diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 32445a618d1..1fce5046f91 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -7,7 +7,7 @@ from frappe.exceptions import ValidationError import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no -from frappe.utils import cint +from frappe.utils import cint, flt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory class TestBatch(unittest.TestCase): @@ -35,12 +35,13 @@ class TestBatch(unittest.TestCase): receipt = frappe.get_doc(dict( doctype='Purchase Receipt', supplier='_Test Supplier', + company='_Test Company', items=[ dict( item_code='ITEM-BATCH-1', qty=batch_qty, rate=10, - warehouse= 'Stores - WP' + warehouse= 'Stores - _TC' ) ] )).insert() @@ -175,6 +176,18 @@ class TestBatch(unittest.TestCase): self.assertEqual(get_batch_qty('batch a', '_Test Warehouse - _TC'), 90) + def test_total_batch_qty(self): + self.make_batch_item('ITEM-BATCH-3') + existing_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) + stock_entry = self.make_new_batch_and_entry('ITEM-BATCH-3', 'B100', '_Test Warehouse - _TC') + + current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) + self.assertEqual(current_batch_qty, existing_batch_qty + 90) + + stock_entry.cancel() + current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) + self.assertEqual(current_batch_qty, existing_batch_qty) + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -208,6 +221,8 @@ class TestBatch(unittest.TestCase): stock_entry.insert() stock_entry.submit() + return stock_entry + def test_batch_name_with_naming_series(self): stock_settings = frappe.get_single('Stock Settings') use_naming_series = cint(stock_settings.use_naming_series) 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 5fe89d6e227..45ed498a28e 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -37,11 +37,19 @@ class StockLedgerEntry(Document): def on_submit(self): self.check_stock_frozen_date() self.actual_amt_check() + self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): from erpnext.stock.doctype.serial_no.serial_no import process_serial_no process_serial_no(self) + def calculate_batch_qty(self): + if self.batch_no: + batch_qty = frappe.db.get_value("Stock Ledger Entry", + {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": "No"}, + "sum(actual_qty)") or 0 + frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) + #check for item quantity available in stock def actual_amt_check(self): if self.batch_no and not self.get("allow_negative_stock"): @@ -139,4 +147,3 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) - diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index afa239466b5..0a49c26b629 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -488,12 +488,14 @@ def get_stock_balance_for(item_code, warehouse, ["has_serial_no", "has_batch_no"], as_dict=1) serial_nos = "" - if item_dict.get("has_serial_no"): - qty, rate, serial_nos = get_qty_rate_for_serial_nos(item_code, - warehouse, posting_date, posting_time, item_dict) + with_serial_no = True if item_dict.get("has_serial_no") else False + data = get_stock_balance(item_code, warehouse, posting_date, posting_time, + with_valuation_rate=with_valuation_rate, with_serial_no=with_serial_no) + + if with_serial_no: + qty, rate, serial_nos = data else: - qty, rate = get_stock_balance(item_code, warehouse, - posting_date, posting_time, with_valuation_rate=with_valuation_rate) + qty, rate = data if item_dict.get("has_batch_no"): qty = get_batch_qty(batch_no, warehouse) or 0 @@ -504,28 +506,6 @@ def get_stock_balance_for(item_code, warehouse, 'serial_nos': serial_nos } -def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time, item_dict): - args = { - "item_code": item_code, - "warehouse": warehouse, - "posting_date": posting_date, - "posting_time": posting_time, - } - - serial_nos_list = [serial_no.get("name") - for serial_no in get_available_serial_nos(args)] - - qty = len(serial_nos_list) - serial_nos = '\n'.join(serial_nos_list) - args.update({ - 'qty': qty, - "serial_nos": serial_nos - }) - - rate = get_incoming_rate(args, raise_error_if_no_rate=False) or 0 - - return qty, rate, serial_nos - @frappe.whitelist() def get_difference_account(purpose, company): if purpose == 'Stock Reconciliation': diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 94ec314e8c9..1af68dd7f22 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -35,7 +35,7 @@ def get_data(report_filters): gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) d.difference_value = (d.stock_value - d.account_value) - if abs(d.difference_value) > 1.0/10 ** currency_precision: + if abs(d.difference_value) > 0.1: data.append(d) return data diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f3381c76091..7f32b8d8bb8 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -74,7 +74,8 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): return sum(sle_map.values()) @frappe.whitelist() -def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, with_valuation_rate=False): +def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, + with_valuation_rate=False, with_serial_no=False): """Returns stock balance quantity at given warehouse on given posting date or current date. If `with_valuation_rate` is True, will return tuple (qty, rate)""" @@ -84,17 +85,51 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None if not posting_date: posting_date = nowdate() if not posting_time: posting_time = nowtime() - last_entry = get_previous_sle({ + args = { "item_code": item_code, "warehouse":warehouse, "posting_date": posting_date, - "posting_time": posting_time }) + "posting_time": posting_time + } + + last_entry = get_previous_sle(args) if with_valuation_rate: - return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + if with_serial_no: + serial_nos = last_entry.get("serial_no") + + if (serial_nos and + len(get_serial_nos_data(serial_nos)) < last_entry.qty_after_transaction): + serial_nos = get_serial_nos_data_after_transactions(args) + + return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) + if last_entry else (0.0, 0.0, 0.0)) + else: + return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) else: return last_entry.qty_after_transaction if last_entry else 0.0 +def get_serial_nos_data_after_transactions(args): + serial_nos = [] + data = frappe.db.sql(""" SELECT serial_no, actual_qty + FROM `tabStock Ledger Entry` + WHERE + item_code = %(item_code)s and warehouse = %(warehouse)s + and timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s) + order by posting_date, posting_time asc """, args, as_dict=1) + + for d in data: + if d.actual_qty > 0: + serial_nos.extend(get_serial_nos_data(d.serial_no)) + else: + serial_nos = list(set(serial_nos) - set(get_serial_nos_data(d.serial_no))) + + return '\n'.join(serial_nos) + +def get_serial_nos_data(serial_nos): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return get_serial_nos(serial_nos) + @frappe.whitelist() def get_latest_stock_qty(item_code, warehouse=None): values, condition = [item_code], ""