diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 96ad60f866f..76123b1a0b8 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.0.18' +__version__ = '10.0.19' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 2cb8e1024a4..ed3de071408 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -254,6 +254,36 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "allow_user_to_edit_discount", + "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": "Allow user to edit Discount", + "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, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1476,7 +1506,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-01-03 17:30:45.198147", + "modified": "2018-01-31 19:33:11.765731", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2a0c553a366..ed2e3e4abdb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -249,7 +249,8 @@ class SalesInvoice(SellingController): if pos: return { "print_format": pos.get("print_format_for_online"), - "allow_edit_rate": pos.get("allow_user_to_edit_rate") + "allow_edit_rate": pos.get("allow_user_to_edit_rate"), + "allow_edit_discount": pos.get("allow_user_to_edit_discount") } def update_time_sheet(self, sales_invoice): diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 558dd8dc1c6..a852b76e6f4 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1620,8 +1620,16 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ var me = this; var invoice_data = {}; this.si_docs = this.get_doc_from_localstorage(); + if (this.frm.doc.offline_pos_name) { - this.update_invoice(); + this.update_invoice() + //to retrieve and set the default payment + invoice_data[this.frm.doc.offline_pos_name] = this.frm.doc; + invoice_data[this.frm.doc.offline_pos_name].payments[0].amount = this.frm.doc.net_total + invoice_data[this.frm.doc.offline_pos_name].payments[0].base_amount = this.frm.doc.net_total + + this.frm.doc.paid_amount = this.frm.doc.net_total + this.frm.doc.outstanding_amount = 0 } else { this.frm.doc.offline_pos_name = $.now(); this.frm.doc.posting_date = frappe.datetime.get_today(); diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index ab5251f292b..8917c9ff631 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -168,11 +168,11 @@ def get_tax_accounts(item_list, columns, company_currency, for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) - item_row_map.setdefault(d.parent, {}).setdefault(d.item_code, []).append(d) + item_row_map.setdefault(d.parent, {}).setdefault(d.item_code or d.item_name, []).append(d) conditions = "" if doctype == "Purchase Invoice": - conditions = " and category in ('Total', 'Valuation and Total')" + conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" tax_details = frappe.db.sql(""" select diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 37848d50643..610475acfcb 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -172,6 +172,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount from `tabPurchase Taxes and Charges` where parent in (%s) and category in ('Total', 'Valuation and Total') + and base_tax_amount_after_discount_amount != 0 group by parent, account_head, add_deduct_tax """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 5df2a6505a3..7142c699555 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -51,8 +51,8 @@ def validate_filters(filters): filters.to_date = filters.year_end_date def get_data(filters): - accounts = frappe.db.sql("""select name, parent_account, account_name, root_type, report_type, lft, rgt - from `tabAccount` where company=%s order by lft""", filters.company, as_dict=True) + accounts = frappe.db.sql("""select name, parent_account, account_name, account_number, root_type, report_type, lft, rgt + from `tabAccount` where company=%s order by account_name, lft""", filters.company, as_dict=True) company_currency = erpnext.get_company_currency(filters.company) if not accounts: @@ -175,6 +175,9 @@ def accumulate_values_into_parents(accounts, accounts_by_name): def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): data = [] + tmpaccnt = sorted(accounts) + if not (accounts[0].account_number is None): + accounts = tmpaccnt for d in accounts: has_value = False diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 8075ab32e42..b4489881815 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -68,12 +68,16 @@ class PurchaseOrder(BuyingController): }, "Supplier Quotation Item": { "ref_dn_field": "supplier_quotation_item", - "compare_fields": [["rate", "="], ["project", "="], ["item_code", "="], + "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="], ["conversion_factor", "="]], "is_child_table": True } }) + + if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')): + self.validate_rate_with_reference_doc([["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]]) + def validate_supplier(self): prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos') if prevent_po: diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index c395d0cfd76..b221a089596 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -124,7 +124,8 @@ def make_purchase_order(source_name, target_doc=None): ["name", "supplier_quotation_item"], ["parent", "supplier_quotation"], ["material_request", "material_request"], - ["material_request_item", "material_request_item"] + ["material_request_item", "material_request_item"], + ["sales_order", "sales_order"] ], "postprocess": update_item }, diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index b3a55b645d0..da78c12ba03 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -1326,6 +1326,37 @@ "unique": 0, "width": "120px" }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_order", + "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": "Sales Order", + "length": 0, + "no_copy": 0, + "options": "Sales Order", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1614,7 +1645,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-12-14 09:37:47.427897", + "modified": "2018-01-25 15:04:40.171617", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py index b97e097f08a..ac11c7e45c2 100644 --- a/erpnext/config/projects.py +++ b/erpnext/config/projects.py @@ -15,7 +15,7 @@ def get_data(): { "type": "doctype", "name": "Task", - "route": "Tree/Task", + "route": "List/Task", "description": _("Project activity / task."), }, { diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ebc1921f120..45abbdb0ec9 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -204,7 +204,7 @@ doc_events = { 'Address': { 'validate': 'erpnext.regional.india.utils.validate_gstin_for_india' }, - 'Sales Invoice': { + ('Sales Invoice', 'Purchase Invoice'): { 'validate': 'erpnext.regional.india.utils.set_place_of_supply' } } diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index fb52d5260cc..2e69475c9a1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -544,7 +544,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite group by item_code, stock_uom order by idx""" - if fetch_exploded: + if cint(fetch_exploded): query = query.format(table="BOM Explosion Item", where_conditions="", select_columns = ", bom_item.source_warehouse, (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s ) as idx") diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f5953e2c492..6f4c63ebe2e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -491,3 +491,5 @@ erpnext.patches.v10_0.update_assessment_result erpnext.patches.v10_0.added_extra_gst_custom_field erpnext.patches.v10_0.workflow_leave_application #2018-01-24 erpnext.patches.v10_0.set_default_payment_terms_based_on_company +erpnext.patches.v10_0.update_sales_order_link_to_purchase_order +erpnext.patches.v10_0.added_extra_gst_custom_field_in_gstr2 diff --git a/erpnext/patches/v10_0/added_extra_gst_custom_field_in_gstr2.py b/erpnext/patches/v10_0/added_extra_gst_custom_field_in_gstr2.py new file mode 100644 index 00000000000..a1512ed9b77 --- /dev/null +++ b/erpnext/patches/v10_0/added_extra_gst_custom_field_in_gstr2.py @@ -0,0 +1,9 @@ +import frappe +from erpnext.regional.india.setup import make_custom_fields + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + make_custom_fields() \ No newline at end of file diff --git a/erpnext/patches/v10_0/update_sales_order_link_to_purchase_order.py b/erpnext/patches/v10_0/update_sales_order_link_to_purchase_order.py new file mode 100644 index 00000000000..b4f58384bfe --- /dev/null +++ b/erpnext/patches/v10_0/update_sales_order_link_to_purchase_order.py @@ -0,0 +1,18 @@ +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("buying", "doctype", "supplier_quotation_item") + + for doctype in ['Purchase Order','Supplier Quotation']: + frappe.db.sql(""" + Update + `tab{doctype} Item`, `tabMaterial Request Item` + set + `tab{doctype} Item`.sales_order = `tabMaterial Request Item`.sales_order + where + `tab{doctype} Item`.material_request= `tabMaterial Request Item`.parent + and `tab{doctype} Item`.material_request_item = `tabMaterial Request Item`.name + and `tabMaterial Request Item`.sales_order is not null""".format(doctype=doctype)) \ No newline at end of file diff --git a/erpnext/public/js/payment/payments.js b/erpnext/public/js/payment/payments.js index 77209e093a8..0d656bc1fb6 100644 --- a/erpnext/public/js/payment/payments.js +++ b/erpnext/public/js/payment/payments.js @@ -27,8 +27,14 @@ erpnext.payments = erpnext.stock.StockController.extend({ var me = this; this.dialog.set_primary_action(__("Submit"), function() { - me.dialog.hide() - me.submit_invoice() + // Allow no ZERO payment + $.each(me.frm.doc.payments, function (index, data) { + if (data.amount != 0) { + me.dialog.hide(); + me.submit_invoice(); + return; + } + }); }) }, diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index e9d91abd488..fa0a44890c9 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -87,16 +87,15 @@ def make_custom_fields(): allow_on_submit=1, print_hide=1) invoice_gst_fields = [ dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='select_print_heading', print_hide=1, collapsible=1), + insert_after='language', print_hide=1, collapsible=1), dict(fieldname='invoice_copy', label='Invoice Copy', fieldtype='Select', insert_after='gst_section', print_hide=1, allow_on_submit=1, options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), dict(fieldname='reverse_charge', label='Reverse Charge', fieldtype='Select', insert_after='invoice_copy', print_hide=1, options='Y\nN', default='N'), - dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='reverse_charge'), dict(fieldname='invoice_type', label='Invoice Type', - fieldtype='Select', insert_after='reverse_charge', print_hide=1, + fieldtype='Select', insert_after='gst_col_break', print_hide=1, options='Regular\nSEZ\nExport\nDeemed Export', default='Regular'), dict(fieldname='export_type', label='Export Type', fieldtype='Select', insert_after='invoice_type', print_hide=1, @@ -104,19 +103,11 @@ def make_custom_fields(): options='\nWith Payment of Tax\nWithout Payment of Tax'), dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', fieldtype='Data', insert_after='export_type', print_hide=1), + dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='reason_for_issuing_document'), dict(fieldname='reason_for_issuing_document', label='Reason For Issuing document', - fieldtype='Select', insert_after='ecommerce_gstin', print_hide=1, - depends_on='eval:doc.is_return==1', - options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others'), - dict(fieldname='port_code', label='Port Code', - fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, - depends_on="eval:doc.invoice_type=='Export' "), - dict(fieldname='shipping_bill_number', label=' Shipping Bill Number', - fieldtype='Data', insert_after='port_code', print_hide=1, - depends_on="eval:doc.invoice_type=='Export' "), - dict(fieldname='shipping_bill_date', label='Shipping Bill Date', - fieldtype='Date', insert_after='shipping_bill_number', print_hide=1, - depends_on="eval:doc.invoice_type=='Export' "), + fieldtype='Select', insert_after='gst_col_break', print_hide=1, + depends_on='eval:doc.is_return==1', reqd=1, + options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others', default='01-Sales Return') ] purchase_invoice_gst_fields = [ @@ -125,7 +116,21 @@ def make_custom_fields(): options='supplier_address.gstin', print_hide=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='shipping_address', - options='shipping_address.gstin', print_hide=1) + options='shipping_address.gstin', print_hide=1), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='shipping_address', + print_hide=1, read_only=0), + dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', + fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, + options='input\ninput service\ncapital goods\nineligible', default="ineligible"), + dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax', + fieldtype='Data', insert_after='eligibility_for_itc', print_hide=1), + dict(fieldname='itc_central_tax', label='Availed ITC Central Tax', + fieldtype='Data', insert_after='itc_integrated_tax', print_hide=1), + dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax', + fieldtype='Data', insert_after='itc_central_tax', print_hide=1), + dict(fieldname='itc_cess_amount', label='Availed ITC Cess', + fieldtype='Data', insert_after='itc_state_tax', print_hide=1), ] sales_invoice_gst_fields = [ @@ -140,7 +145,16 @@ def make_custom_fields(): print_hide=1, read_only=0), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='company_address', - options='company_address.gstin', print_hide=1) + options='company_address.gstin', print_hide=1), + dict(fieldname='port_code', label='Port Code', + fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, + depends_on="eval:doc.invoice_type=='Export' "), + dict(fieldname='shipping_bill_number', label=' Shipping Bill Number', + fieldtype='Data', insert_after='port_code', print_hide=1, + depends_on="eval:doc.invoice_type=='Export' "), + dict(fieldname='shipping_bill_date', label='Shipping Bill Date', + fieldtype='Date', insert_after='shipping_bill_number', print_hide=1, + depends_on="eval:doc.invoice_type=='Export' ") ] custom_fields = { @@ -152,8 +166,8 @@ def make_custom_fields(): dict(fieldname='gst_state_number', label='GST State Number', fieldtype='Int', insert_after='gst_state', read_only=1), ], - 'Purchase Invoice': purchase_invoice_gst_fields + invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_fields + invoice_gst_fields, + 'Purchase Invoice': invoice_gst_fields + purchase_invoice_gst_fields, + 'Sales Invoice': invoice_gst_fields + sales_invoice_gst_fields, "Delivery Note": sales_invoice_gst_fields, 'Item': [ dict(fieldname='gst_hsn_code', label='HSN/SAC', diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fb421fb1261..1b912183b10 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -64,7 +64,11 @@ def get_itemised_tax_breakup_data(doc): def set_place_of_supply(doc, method): if not frappe.get_meta('Address').has_field('gst_state'): return - address_name = doc.shipping_address_name or doc.customer_address + if doc.doctype == "Sales Invoice": + address_name = doc.shipping_address_name or doc.customer_address + elif doc.doctype == "Purchase Invoice": + address_name = doc.shipping_address or doc.supplier_address + if address_name: address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) doc.place_of_supply = cstr(address.gst_state_number) + "-" + cstr(address.gst_state) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index b6df878690e..1ad673f2898 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -12,15 +12,32 @@ def execute(filters=None): class Gstr1Report(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) + self.doctype = "Sales Invoice" + self.tax_doctype = "Sales Taxes and Charges" + self.select_columns = """ + name as invoice_number, + customer_name, + posting_date, + base_grand_total, + base_rounded_total, + customer_gstin, + place_of_supply, + ecommerce_gstin, + reverse_charge, + invoice_type, + return_against, + is_return, + invoice_type, + export_type, + port_code, + shipping_bill_number, + shipping_bill_date, + reason_for_issuing_document + """ self.customer_type = "Company" if self.filters.get("type_of_business") == "B2B" else "Individual" def run(self): self.get_columns() - self.get_data() - return self.columns, self.data - - def get_data(self): - self.data = [] self.get_gst_accounts() self.get_invoice_data() @@ -28,27 +45,16 @@ class Gstr1Report(object): self.get_invoice_items() self.get_items_based_on_tax_rate() - invoice_fields = [d["fieldname"] for d in self.invoice_columns] - + self.invoice_fields = [d["fieldname"] for d in self.invoice_columns] + self.get_data() + return self.columns, self.data + def get_data(self): + self.data = [] for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - row = [] - for fieldname in invoice_fields: - if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value": - row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) - elif fieldname == "invoice_value": - row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) - else: - row.append(invoice_details.get(fieldname)) - - row += [rate, - sum([abs(net_amount) for item_code, net_amount in self.invoice_items.get(inv).items() - if item_code in items]), - self.invoice_cess.get(inv) - ] - + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) if self.filters.get("type_of_business") == "B2C Small": row.append("E" if invoice_details.ecommerce_gstin else "OE") @@ -58,34 +64,33 @@ class Gstr1Report(object): self.data.append(row) + def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): + row = [] + for fieldname in self.invoice_fields: + if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value": + row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) + elif fieldname == "invoice_value": + row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) + else: + row.append(invoice_details.get(fieldname)) + + taxable_value = sum([abs(net_amount) + for item_code, net_amount in self.invoice_items.get(invoice).items() if item_code in items]) + row += [tax_rate, taxable_value] + + return row, taxable_value + def get_invoice_data(self): self.invoices = frappe._dict() conditions = self.get_conditions() - invoice_data = frappe.db.sql(""" select - name as invoice_number, - customer_name, - posting_date, - base_grand_total, - base_rounded_total, - customer_gstin, - place_of_supply, - ecommerce_gstin, - reverse_charge, - invoice_type, - return_against, - is_return, - invoice_type, - export_type, - port_code, - shipping_bill_number, - shipping_bill_date, - reason_for_issuing_document - from `tabSales Invoice` - where docstatus = 1 %s + {select_columns} + from `tab{doctype}` + where docstatus = 1 {where_conditions} order by posting_date desc - """ % (conditions), self.filters, as_dict=1) + """.format(select_columns=self.select_columns, doctype=self.doctype, + where_conditions=conditions), self.filters, as_dict=1) for d in invoice_data: self.invoices.setdefault(d.invoice_number, d) @@ -127,30 +132,29 @@ class Gstr1Report(object): self.invoice_items = frappe._dict() items = frappe.db.sql(""" select item_code, parent, base_net_amount - from `tabSales Invoice Item` + from `tab%s Item` where parent in (%s) - """ % (', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) for d in items: self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, d.base_net_amount) def get_items_based_on_tax_rate(self): - tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql(""" select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount - from `tabSales Taxes and Charges` + from `tab%s` where - parenttype = 'Sales Invoice' and docstatus = 1 + parenttype = %s and docstatus = 1 and parent in (%s) - order by account_head - """ % (', '.join(['%s']*len(self.invoices.keys()))), tuple(self.invoices.keys())) + """ % (self.tax_doctype, '%s', ', '.join(['%s']*len(self.invoices.keys()))), + tuple([self.doctype] + self.invoices.keys())) self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() unidentified_gst_accounts = [] - - for parent, account, item_wise_tax_detail, tax_amount in tax_details: + for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if account in self.gst_accounts.cess_account: self.invoice_cess.setdefault(parent, tax_amount) else: @@ -172,8 +176,8 @@ class Gstr1Report(object): if cgst_or_sgst: tax_rate *= 2 - rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {})\ - .setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate\ + .setdefault(parent, {}).setdefault(tax_rate, []) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: diff --git a/erpnext/regional/report/gstr_2/__init__.py b/erpnext/regional/report/gstr_2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/gstr_2/gstr_2.js b/erpnext/regional/report/gstr_2/gstr_2.js new file mode 100644 index 00000000000..5c1ea67e8ec --- /dev/null +++ b/erpnext/regional/report/gstr_2/gstr_2.js @@ -0,0 +1,39 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["GSTR-2"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -3), + "width": "80" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"type_of_business", + "label": __("Type of Business"), + "fieldtype": "Select", + "reqd": 1, + "options": ["B2B","CDNR"], + "default": "B2B" + } + ] +} diff --git a/erpnext/regional/report/gstr_2/gstr_2.json b/erpnext/regional/report/gstr_2/gstr_2.json new file mode 100644 index 00000000000..929ed914d83 --- /dev/null +++ b/erpnext/regional/report/gstr_2/gstr_2.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2018-01-29 12:59:55.650445", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-01-29 12:59:55.650445", + "modified_by": "Administrator", + "module": "Regional", + "name": "GSTR-2", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "GSTR-2", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py new file mode 100644 index 00000000000..a69a67f53be --- /dev/null +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -0,0 +1,276 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from datetime import date +from erpnext.regional.report.gstr_1.gstr_1 import Gstr1Report + +def execute(filters=None): + return Gstr2Report(filters).run() + +class Gstr2Report(Gstr1Report): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.doctype = "Purchase Invoice" + self.tax_doctype = "Purchase Taxes and Charges" + self.select_columns = """ + name as invoice_number, + supplier_name, + posting_date, + base_grand_total, + base_rounded_total, + supplier_gstin, + place_of_supply, + ecommerce_gstin, + reverse_charge, + invoice_type, + return_against, + is_return, + invoice_type, + export_type, + reason_for_issuing_document, + eligibility_for_itc, + itc_integrated_tax, + itc_central_tax, + itc_state_tax, + itc_cess_amount + """ + + def get_data(self): + self.get_igst_invoices() + self.data = [] + for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): + invoice_details = self.invoices.get(inv) + for rate, items in items_based_on_rate.items(): + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + if inv in self.igst_invoices: + row += [tax_amount, 0, 0] + else: + row += [0, tax_amount / 2, tax_amount / 2] + + row += [ + self.invoice_cess.get(inv), + invoice_details.get('eligibility_for_itc'), + invoice_details.get('itc_integrated_tax'), + invoice_details.get('itc_central_tax'), + invoice_details.get('itc_state_tax'), + invoice_details.get('itc_cess_amount') + ] + if self.filters.get("type_of_business") == "CDNR": + row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N") + row.append("C" if invoice_details.return_against else "R") + + self.data.append(row) + + def get_igst_invoices(self): + self.igst_invoices = [] + for d in self.tax_details: + is_igst = True if d[1] in self.gst_accounts.igst_account else False + if is_igst and d[0] not in self.igst_invoices: + self.igst_invoices.append(d[0]) + if is_igst: + break + + def get_conditions(self): + conditions = "" + + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if self.filters.get(opts[0]): + conditions += opts[1] + + if self.filters.get("type_of_business") == "B2B": + conditions += "and invoice_type != 'Export' and is_return != 1 " + + elif self.filters.get("type_of_business") == "CDNR": + conditions += """ and is_return = 1 """ + + return conditions + + def get_columns(self): + self.tax_columns = [ + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Int", + "width": 60 + }, + { + "fieldname": "taxable_value", + "label": "Taxable Value", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "integrated_tax_paid", + "label": "Integrated Tax Paid", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "central_tax_paid", + "label": "Central Tax Paid", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "state_tax_paid", + "label": "State/UT Tax Paid", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "cess_amount", + "label": "Cess Paid", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "eligibility_for_itc", + "label": "Eligibility For ITC", + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "itc_integrated_tax", + "label": "Availed ITC Integrated Tax", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "itc_central_tax", + "label": "Availed ITC Central Tax", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "itc_state_tax", + "label": "Availed ITC State/UT Tax", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "itc_cess_amount", + "label": "Availed ITC Cess ", + "fieldtype": "Currency", + "width": 100 + } + ] + self.other_columns = [] + + if self.filters.get("type_of_business") == "B2B": + self.invoice_columns = [ + { + "fieldname": "supplier_gstin", + "label": "GSTIN of Supplier", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "invoice_number", + "label": "Invoice Number", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 120 + }, + { + "fieldname": "posting_date", + "label": "Invoice date", + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "invoice_value", + "label": "Invoice Value", + "fieldtype": "Currency", + "width": 120 + }, + { + "fieldname": "place_of_supply", + "label": "Place of Supply", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "reverse_charge", + "label": "Reverse Charge", + "fieldtype": "Data", + "width": 80 + }, + { + "fieldname": "invoice_type", + "label": "Invoice Type", + "fieldtype": "Data", + "width": 80 + } + ] + elif self.filters.get("type_of_business") == "CDNR": + self.invoice_columns = [ + { + "fieldname": "supplier_gstin", + "label": "GSTIN of Supplier", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "invoice_number", + "label": "Note/Refund Voucher Number", + "fieldtype": "Link", + "options": "Purchase Invoice" + }, + { + "fieldname": "posting_date", + "label": "Note/Refund Voucher date", + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "return_against", + "label": "Invoice/Advance Payment Voucher Number", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 120 + }, + { + "fieldname": "posting_date", + "label": "Invoice/Advance Payment Voucher date", + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "reason_for_issuing_document", + "label": "Reason For Issuing document", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "supply_type", + "label": "Supply Type", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "invoice_value", + "label": "Invoice Value", + "fieldtype": "Currency", + "width": 120 + } + ] + self.other_columns = [ + { + "fieldname": "pre_gst", + "label": "PRE GST", + "fieldtype": "Data", + "width": 50 + }, + { + "fieldname": "document_type", + "label": "Document Type", + "fieldtype": "Data", + "width": 50 + } + ] + self.columns = self.invoice_columns + self.tax_columns + self.other_columns diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 2da5431e980..5c2662c36e7 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -469,6 +469,7 @@ erpnext.pos.PointOfSale = class PointOfSale { if (r.message) { this.frm.meta.default_print_format = r.message.print_format || 'POS Invoice'; this.frm.allow_edit_rate = r.message.allow_edit_rate; + this.frm.allow_edit_discount = r.message.allow_edit_discount; } } @@ -565,9 +566,9 @@ class POSCart {
${this.get_taxes_and_totals()}
-
- ${this.get_discount_amount()} -
+
`+ + (!this.frm.allow_edit_discount ? `` : `${this.get_discount_amount()}`)+ + `
${this.get_grand_total()}
@@ -596,6 +597,7 @@ class POSCart { this.numpad && this.numpad.reset_value(); this.customer_field.set_value(""); + this.$discount_amount.find('input:text').val(''); this.wrapper.find('.grand-total-value').text( format_currency(this.frm.doc.grand_total, this.frm.currency)); this.wrapper.find('.rounded-total-value').text( @@ -726,6 +728,17 @@ class POSCart { this.customer_field.set_value(this.frm.doc.customer); } + disable_numpad_control() { + let disabled_btns = []; + if(!this.frm.allow_edit_rate) { + disabled_btns.push('Rate'); + } + if(!this.frm.allow_edit_discount) { + disabled_btns.push('Disc'); + } + return disabled_btns; + } + make_numpad() { this.numpad = new NumberPad({ button_array: [ @@ -740,7 +753,7 @@ class POSCart { disable_highlight: ['Qty', 'Disc', 'Rate', 'Pay'], reset_btns: ['Qty', 'Disc', 'Rate', 'Pay'], del_btn: 'Del', - disable_btns: !this.frm.allow_edit_rate ? ['Rate']: [], + disable_btns: this.disable_numpad_control(), wrapper: this.wrapper.find('.number-pad-container'), onclick: (btn_value) => { // on click @@ -1280,7 +1293,7 @@ class NumberPad { this.disable_highlight = disable_highlight; this.reset_btns = reset_btns; this.del_btn = del_btn; - this.disable_btns = disable_btns; + this.disable_btns = disable_btns || []; this.make_dom(); this.bind_events(); this.value = ''; @@ -1312,13 +1325,15 @@ class NumberPad { this.set_class(); - this.disable_btns.forEach((btn) => { - const $btn = this.get_btn(btn); - $btn.prop("disabled", true) - $btn.hover(() => { - $btn.css('cursor','not-allowed'); + if(this.disable_btns) { + this.disable_btns.forEach((btn) => { + const $btn = this.get_btn(btn); + $btn.prop("disabled", true) + $btn.hover(() => { + $btn.css('cursor','not-allowed'); + }) }) - }) + } } set_class() { diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 39172d7d11b..14a478d7e4e 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -9,6 +9,7 @@ from frappe.utils.nestedset import NestedSet from frappe.website.website_generator import WebsiteGenerator from frappe.website.render import clear_cache from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow +from erpnext.utilities.product import get_qty_in_stock class ItemGroup(NestedSet, WebsiteGenerator): @@ -83,7 +84,8 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non # base query query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group, I.description, I.web_long_description as website_description, I.is_stock_item, - case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock + case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse, + I.has_batch_no from `tabItem` I left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse where I.show_in_website = 1 @@ -104,8 +106,26 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1) + data = adjust_qty_for_expired_items(data) + return [get_item_for_list_in_html(r) for r in data] + +def adjust_qty_for_expired_items(data): + adjusted_data = [] + + for item in data: + if item.get('has_batch_no') and item.get('website_warehouse'): + stock_qty_dict = get_qty_in_stock( + item.get('name'), 'website_warehouse', item.get('website_warehouse')) + qty = stock_qty_dict.stock_qty[0][0] + item['in_stock'] = 1 if qty else 0 + adjusted_data.append(item) + + return adjusted_data + + + def get_child_groups(item_group_name): item_group = frappe.get_doc("Item Group", item_group_name) return frappe.db.sql("""select name diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1673a96ef6b..b5674d3cfb1 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -131,7 +131,7 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None): :param batch_no: Optional - give qty for this batch no :param warehouse: Optional - give qty for this warehouse :param item_code: Optional - give qty for this item""" - frappe.has_permission('Batch', throw=True) + out = 0 if batch_no and warehouse: out = float(frappe.db.sql("""select sum(actual_qty) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index da310aa3d7a..defce62e2ea 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -65,7 +65,7 @@ class MaterialRequest(BuyingController): self.status = "Draft" from erpnext.controllers.status_updater import validate_status - validate_status(self.status, + validate_status(self.status, ["Draft", "Submitted", "Stopped", "Cancelled", "Pending", "Partially Ordered", "Ordered", "Issued", "Transferred"]) @@ -240,7 +240,8 @@ def make_purchase_order(source_name, target_doc=None): ["name", "material_request_item"], ["parent", "material_request"], ["uom", "stock_uom"], - ["uom", "uom"] + ["uom", "uom"], + ["sales_order", "sales_order"] ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty @@ -344,7 +345,8 @@ def make_supplier_quotation(source_name, target_doc=None): "doctype": "Supplier Quotation Item", "field_map": { "name": "material_request_item", - "parent": "material_request" + "parent": "material_request", + "sales_order": "sales_order" } } }, target_doc, postprocess) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a7638b4169e..6b6723331ab 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -472,8 +472,8 @@ def get_projected_qty(item_code, warehouse): @frappe.whitelist() def get_bin_details(item_code, warehouse): return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty"], as_dict=True) \ - or {"projected_qty": 0, "actual_qty": 0} + ["projected_qty", "actual_qty", "ordered_qty"], as_dict=True) \ + or {"projected_qty": 0, "actual_qty": 0, "ordered_qty": 0} @frappe.whitelist() def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 8377f599791..e436132ec87 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -106,11 +106,10 @@ def get_opening_balance(filters, columns): from erpnext.stock.stock_ledger import get_previous_sle last_entry = get_previous_sle({ "item_code": filters.item_code, - "warehouse": get_warehouse_condition(filters.warehouse), + "warehouse_condition": get_warehouse_condition(filters.warehouse), "posting_date": filters.from_date, "posting_time": "00:00:00" }) - row = [""]*len(columns) row[1] = _("'Opening'") for i, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')): @@ -122,7 +121,7 @@ def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: return " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"%(warehouse_details.lft, + where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)"%(warehouse_details.lft, warehouse_details.rgt) return '' diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b5a2d78d3da..956f976d705 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -407,7 +407,12 @@ def get_previous_sle(args, for_update=False): def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False, debug=False): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = "timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) + conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) + if previous_sle.get("warehouse"): + conditions += " and warehouse = %(warehouse)s" + elif previous_sle.get("warehouse_condition"): + conditions += " and " + previous_sle.get("warehouse_condition") + if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" if not previous_sle.get("posting_time"): @@ -418,9 +423,8 @@ def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=No return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s - and warehouse = %%(warehouse)s and ifnull(is_cancelled, 'No')='No' - and %(conditions)s + %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, name %(order)s %(limit)s %(for_update)s""" % { "conditions": conditions, diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 9d5c056ba7e..415056d80bc 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -4,25 +4,70 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, fmt_money, flt +from frappe.utils import cint, fmt_money, flt, nowdate, getdate from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item +from erpnext.stock.doctype.batch.batch import get_batch_qty -def get_qty_in_stock(item_code, item_warehouse_field): +def get_qty_in_stock(item_code, item_warehouse_field, warehouse=None): in_stock, stock_qty = 0, '' template_item_code, is_stock_item = frappe.db.get_value("Item", item_code, ["variant_of", "is_stock_item"]) - warehouse = frappe.db.get_value("Item", item_code, item_warehouse_field) + if not warehouse: + warehouse = frappe.db.get_value("Item", item_code, item_warehouse_field) + if not warehouse and template_item_code and template_item_code != item_code: warehouse = frappe.db.get_value("Item", template_item_code, item_warehouse_field) if warehouse: stock_qty = frappe.db.sql("""select GREATEST(actual_qty - reserved_qty, 0) from tabBin where item_code=%s and warehouse=%s""", (item_code, warehouse)) - if stock_qty: + + if stock_qty: + stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse) + + if stock_qty: + in_stock = stock_qty[0][0] > 0 and 1 or 0 + + if stock_qty: in_stock = stock_qty[0][0] > 0 and 1 or 0 return frappe._dict({"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item}) + +def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): + batches = frappe.get_all('Batch', filters=[{'item': item_code}], fields=['expiry_date', 'name']) + expired_batches = get_expired_batches(batches) + stock_qty = [list(item) for item in stock_qty] + + for batch in expired_batches: + if warehouse: + stock_qty[0][0] = max(0, stock_qty[0][0] - get_batch_qty(batch, warehouse)) + else: + stock_qty[0][0] = max(0, stock_qty[0][0] - qty_from_all_warehouses(get_batch_qty(batch))) + + if not stock_qty[0][0]: + break + + return stock_qty + + +def get_expired_batches(batches): + """ + :param batches: A list of dict in the form [{'expiry_date': datetime.date(20XX, 1, 1), 'name': 'batch_id'}, ...] + """ + return [b.name for b in batches if b.expiry_date and b.expiry_date <= getdate(nowdate())] + + +def qty_from_all_warehouses(batch_info): + """ + :param batch_info: A list of dict in the form [{u'warehouse': u'Stores - I', u'qty': 0.8}, ...] + """ + qty = 0 + for batch in batch_info: + qty = qty + batch.qty + + return qty + def get_price(item_code, price_list, customer_group, company, qty=1): template_item_code = frappe.db.get_value("Item", item_code, "variant_of")