diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 6f8878d2167..e4ba67017e6 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -7,7 +7,7 @@ import frappe
from erpnext.hooks import regional_overrides
-__version__ = '13.12.0'
+__version__ = '13.12.1'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index d6e41e6f90d..27d678b212d 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -40,6 +40,7 @@ class POSInvoice(SalesInvoice):
self.validate_change_amount()
self.validate_change_account()
self.validate_item_cost_centers()
+ self.validate_warehouse()
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 462ee649139..67465c51acd 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1441,15 +1441,22 @@ class TestSalesInvoice(unittest.TestCase):
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = {
- "999800": {
+ "_Test Item": {
"Service Tax": {
"tax_rate": 10.0,
- "tax_amount": 1500.0
+ "tax_amount": 1000.0
+ }
+ },
+ "_Test Item 2": {
+ "Service Tax": {
+ "tax_rate": 10.0,
+ "tax_amount": 500.0
}
}
}
expected_itemised_taxable_amount = {
- "999800": 15000.0
+ "_Test Item": 10000.0,
+ "_Test Item 2": 5000.0
}
self.assertEqual(itemised_tax, expected_itemised_tax)
diff --git a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
index 8ccef6a8172..afe273f3102 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
+++ b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
@@ -89,13 +89,14 @@
"width": "160px"
},
{
+ "allow_on_submit": 1,
"columns": 2,
+ "default": "Pending",
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
- "options": "Pending\nPartially Completed\nFully Completed",
- "read_only": 1
+ "options": "Pending\nPartially Completed\nFully Completed"
},
{
"fieldname": "column_break_3",
@@ -125,10 +126,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-27 16:07:25.905015",
+ "modified": "2021-09-16 21:25:22.506485",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index c43aa31ee39..71d5335d518 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -314,7 +314,10 @@ erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
+erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
+erpnext.patches.v13_0.shopping_cart_to_ecommerce
+erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
diff --git a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py
new file mode 100644
index 00000000000..5b790d9f173
--- /dev/null
+++ b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py
@@ -0,0 +1,44 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
+ if not company:
+ return
+
+ hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
+ fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
+ allow_on_submit=1, print_hide=1, fetch_if_empty=1)
+ nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted',
+ fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code',
+ print_hide=1)
+ is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
+ fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
+ print_hide=1)
+ taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
+ fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
+ print_hide=1)
+ sales_invoice_gst_fields = [
+ dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
+ fieldtype='Data', insert_after='customer_address', read_only=1,
+ fetch_from='customer_address.gstin', print_hide=1),
+ dict(fieldname='customer_gstin', label='Customer GSTIN',
+ fieldtype='Data', insert_after='shipping_address_name',
+ fetch_from='shipping_address_name.gstin', print_hide=1),
+ dict(fieldname='place_of_supply', label='Place of Supply',
+ fieldtype='Data', insert_after='customer_gstin',
+ print_hide=1, read_only=1),
+ dict(fieldname='company_gstin', label='Company GSTIN',
+ fieldtype='Data', insert_after='company_address',
+ fetch_from='company_address.gstin', print_hide=1, read_only=1),
+ ]
+
+ custom_fields = {
+ 'POS Invoice': sales_invoice_gst_fields,
+ 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
+ }
+
+ create_custom_fields(custom_fields, update=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
new file mode 100644
index 00000000000..9887ad9df0c
--- /dev/null
+++ b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
@@ -0,0 +1,10 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail")
+ frappe.db.sql("""
+ UPDATE `tabMaintenance Schedule Detail`
+ SET completion_status = 'Pending'
+ WHERE docstatus < 2
+ """)
diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
new file mode 100644
index 00000000000..d336c929751
--- /dev/null
+++ b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
@@ -0,0 +1,29 @@
+import click
+import frappe
+
+
+def execute():
+
+ frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True)
+ frappe.delete_doc("DocType", "Products Settings", ignore_missing=True)
+ frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True)
+
+ if frappe.db.get_single_value("E Commerce Settings", "enabled"):
+ notify_users()
+
+
+def notify_users():
+
+ click.secho(
+ "Shopping cart and Product settings are merged into E-commerce settings.\n"
+ "Checkout the documentation to learn more:"
+ "https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce",
+ fg="yellow",
+ )
+
+ note = frappe.new_doc("Note")
+ note.title = "New E-Commerce Module"
+ note.public = 1
+ note.notify_on_login = 1
+ note.content = """
"""
+ note.save()
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 293a6bac2d8..555f8d04cc2 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -345,26 +345,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
scan_barcode: function() {
- let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
-
- let show_description = function(idx, exist = null) {
- if (exist) {
- frappe.show_alert({
- message: __('Row #{0}: Qty increased by 1', [idx]),
- indicator: 'green'
- });
- } else {
- frappe.show_alert({
- message: __('Row #{0}: Item added', [idx]),
- indicator: 'green'
- });
- }
- }
+ let me = this;
if(this.frm.doc.scan_barcode) {
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
- args: { search_value: this.frm.doc.scan_barcode }
+ args: {
+ search_value: this.frm.doc.scan_barcode
+ }
}).then(r => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
@@ -375,50 +363,97 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
return;
}
- let cur_grid = this.frm.fields_dict.items.grid;
-
- let row_to_modify = null;
- const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
- const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
-
- if (existing_item_row) {
- row_to_modify = existing_item_row;
- } else if (blank_item_row) {
- row_to_modify = blank_item_row;
- }
-
- if (!row_to_modify) {
- // add new row
- row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
- }
-
- show_description(row_to_modify.idx, row_to_modify.item_code);
-
- this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
- frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
- item_code: data.item_code,
- qty: (row_to_modify.qty || 0) + 1
- });
-
- ['serial_no', 'batch_no', 'barcode'].forEach(field => {
- if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
-
- let value = (row_to_modify[field] && field === "serial_no")
- ? row_to_modify[field] + '\n' + data[field] : data[field];
-
- frappe.model.set_value(row_to_modify.doctype,
- row_to_modify.name, field, value);
- }
- });
-
- scan_barcode_field.set_value('');
- refresh_field("items");
+ me.modify_table_after_scan(data);
});
}
return false;
},
- apply_default_taxes: function() {
+ modify_table_after_scan(data) {
+ let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
+ let cur_grid = this.frm.fields_dict.items.grid;
+ let row_to_modify = null;
+
+ // Check if batch is scanned and table has batch no field
+ let batch_no_scan = Boolean(data.batch_no) && frappe.meta.has_field(cur_grid.doctype, "batch_no");
+
+ if (batch_no_scan) {
+ row_to_modify = this.get_batch_row_to_modify(data.batch_no);
+ } else {
+ // serial or barcode scan
+ row_to_modify = this.get_row_to_modify_on_scan(row_to_modify, data);
+ }
+
+ if (!row_to_modify) {
+ // add new row if new item/batch is scanned
+ row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
+ }
+
+ this.show_scan_message(row_to_modify.idx, row_to_modify.item_code);
+ this.set_scanned_values(row_to_modify, data, scan_barcode_field);
+ },
+
+ set_scanned_values(row_to_modify, data, scan_barcode_field) {
+ // increase qty and set scanned value and item in row
+ this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
+ item_code: data.item_code,
+ qty: (row_to_modify.qty || 0) + 1
+ });
+
+ ['serial_no', 'batch_no', 'barcode'].forEach(field => {
+ if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
+ let is_serial_no = row_to_modify[field] && field === "serial_no";
+ let value = data[field];
+
+ if (is_serial_no) {
+ value = row_to_modify[field] + '\n' + data[field];
+ }
+
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, field, value);
+ }
+ });
+
+ scan_barcode_field.set_value('');
+ refresh_field("items");
+ },
+
+ get_row_to_modify_on_scan(row_to_modify, data) {
+ // get an existing item row to increment or blank row to modify
+ const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
+ const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
+
+ if (existing_item_row) {
+ row_to_modify = existing_item_row;
+ } else if (blank_item_row) {
+ row_to_modify = blank_item_row;
+ }
+
+ return row_to_modify;
+ },
+
+ get_batch_row_to_modify(batch_no) {
+ // get row if batch already exists in table
+ const existing_batch_row = this.frm.doc.items.find(d => d.batch_no === batch_no);
+ return existing_batch_row || null;
+ },
+
+ show_scan_message (idx, exist = null) {
+ // show new row or qty increase toast
+ if (exist) {
+ frappe.show_alert({
+ message: __('Row #{0}: Qty increased by 1', [idx]),
+ indicator: 'green'
+ });
+ } else {
+ frappe.show_alert({
+ message: __('Row #{0}: Item added', [idx]),
+ indicator: 'green'
+ });
+ }
+ },
+
+ apply_default_taxes() {
var me = this;
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
me.frm.doc.name);
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index a377b6aa1ed..ded0dae0d80 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -530,6 +530,7 @@ def make_custom_fields(update=True):
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
+ 'POS Invoice': sales_invoice_gst_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Payment Entry': payment_entry_fields,
'Journal Entry': journal_entry_fields,
@@ -548,6 +549,7 @@ def make_custom_fields(update=True):
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
+ 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 45f924694c6..38e17705a6a 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -114,7 +114,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
return [_("Item"), _("Taxable Amount")] + tax_accounts
-def get_itemised_tax_breakup_data(doc, account_wise=False):
+def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise)
itemised_taxable_amount = get_itemised_taxable_amount(doc.items)
@@ -122,28 +122,29 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'):
return itemised_tax, itemised_taxable_amount
- item_hsn_map = frappe._dict()
- for d in doc.items:
- item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
+ if hsn_wise:
+ item_hsn_map = frappe._dict()
+ for d in doc.items:
+ item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
hsn_tax = {}
for item, taxes in itemised_tax.items():
- hsn_code = item_hsn_map.get(item)
- hsn_tax.setdefault(hsn_code, frappe._dict())
+ item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
+ hsn_tax.setdefault(item_or_hsn, frappe._dict())
for tax_desc, tax_detail in taxes.items():
key = tax_desc
if account_wise:
key = tax_detail.get('tax_account')
- hsn_tax[hsn_code].setdefault(key, {"tax_rate": 0, "tax_amount": 0})
- hsn_tax[hsn_code][key]["tax_rate"] = tax_detail.get("tax_rate")
- hsn_tax[hsn_code][key]["tax_amount"] += tax_detail.get("tax_amount")
+ hsn_tax[item_or_hsn].setdefault(key, {"tax_rate": 0, "tax_amount": 0})
+ hsn_tax[item_or_hsn][key]["tax_rate"] = tax_detail.get("tax_rate")
+ hsn_tax[item_or_hsn][key]["tax_amount"] += tax_detail.get("tax_amount")
# set taxable amount
hsn_taxable_amount = frappe._dict()
for item in itemised_taxable_amount:
- hsn_code = item_hsn_map.get(item)
- hsn_taxable_amount.setdefault(hsn_code, 0)
- hsn_taxable_amount[hsn_code] += itemised_taxable_amount.get(item)
+ item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
+ hsn_taxable_amount.setdefault(item_or_hsn, 0)
+ hsn_taxable_amount[item_or_hsn] += itemised_taxable_amount.get(item)
return hsn_tax, hsn_taxable_amount
@@ -440,7 +441,7 @@ def get_ewb_data(dt, dn):
data.itemList = []
data.totalValue = doc.total
- data = get_item_list(data, doc)
+ data = get_item_list(data, doc, hsn_wise=True)
disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total')
data.totInvValue = doc.grand_total if disable_rounded else doc.rounded_total
@@ -551,7 +552,7 @@ def get_address_details(data, doc, company_address, billing_address, dispatch_ad
return data
-def get_item_list(data, doc):
+def get_item_list(data, doc, hsn_wise=False):
for attr in ['cgstValue', 'sgstValue', 'igstValue', 'cessValue', 'OthValue']:
data[attr] = 0
@@ -563,7 +564,7 @@ def get_item_list(data, doc):
'cess_account': ['cessRate', 'cessValue']
}
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
- hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True)
+ hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
for hsn_code, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict()
if not hsn_code: