Merge pull request #30938 from rohitwaghchaure/formatting-for-to-discuss-field
fix: allow to use formatting for the field to_discuss in opportunity
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index f2a696d..a0c0ecc 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2648,6 +2648,7 @@
# reset
einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
+ einvoice_settings.save()
frappe.flags.country = country
def test_einvoice_json(self):
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py
index 5bfca96..4d20129 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py
@@ -163,17 +163,15 @@
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
- from_date = to_date = posting_date
- if not posting_date:
- from_date = "1900-01-01"
- to_date = "4000-01-01"
+ conditions = []
- conditions = [
- """(from_date is null or from_date <= '{0}')
- and (to_date is null or to_date >= '{1}')""".format(
- from_date, to_date
+ if posting_date:
+ conditions.append(
+ f"""(from_date is null or from_date <= '{posting_date}')
+ and (to_date is null or to_date >= '{posting_date}')"""
)
- ]
+ else:
+ conditions.append("(from_date is null) and (to_date is null)")
conditions.append(
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py
index 1bda0d8..9c0aba3 100644
--- a/erpnext/accounts/report/pos_register/pos_register.py
+++ b/erpnext/accounts/report/pos_register/pos_register.py
@@ -62,7 +62,7 @@
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
- p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount,
+ p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
p.customer, p.is_return {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 437ebea..30d9ffc 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -139,7 +139,7 @@
tax_rule_master = set_taxes(
quotation.party_name,
"Customer",
- quotation.transaction_date,
+ None,
quotation.company,
customer_group=None,
supplier_group=None,
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 27479a5..8fae2a9 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -254,7 +254,18 @@
# Adding a day to include To Date in the difference
date_difference = date_diff(self.to_date, self.from_date) + 1
if date_difference < self.total_leaves_allocated:
- frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
+ if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"):
+ frappe.msgprint(
+ _("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
+ indicator="orange",
+ alert=True,
+ )
+ else:
+ frappe.throw(
+ _("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
+ exc=OverAllocationError,
+ title=_("Over Allocation"),
+ )
def create_leave_ledger_entry(self, submit=True):
if self.unused_leaves:
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index a1d39d4..b4a42d3 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -69,22 +69,44 @@
self.assertRaises(frappe.ValidationError, doc.save)
def test_validation_for_over_allocation(self):
+ leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
+ leave_type.save()
+
doc = frappe.get_doc(
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
- "leave_type": "_Test Leave Type",
+ "leave_type": leave_type.name,
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35,
+ "carry_forward": 1,
}
)
# allocated leave more than period
self.assertRaises(OverAllocationError, doc.save)
+ leave_type.allow_over_allocation = 1
+ leave_type.save()
+
+ # allows creating a leave allocation with more leave days than period days
+ doc = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
+ "leave_type": leave_type.name,
+ "from_date": getdate("2015-09-1"),
+ "to_date": getdate("2015-09-30"),
+ "new_leaves_allocated": 35,
+ "carry_forward": 1,
+ }
+ ).insert()
+
def test_validation_for_over_allocation_post_submission(self):
allocation = frappe.get_doc(
{
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 06ca4cd..d40ff09 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -19,6 +19,7 @@
"fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
+ "allow_over_allocation",
"include_holiday",
"is_compensatory",
"carry_forward_section",
@@ -211,15 +212,23 @@
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
"mandatory_depends_on": "eval:doc.is_ppl == 1"
+ },
+ {
+ "default": "0",
+ "description": "Allows allocating more leaves than the number of days in the allocation period.",
+ "fieldname": "allow_over_allocation",
+ "fieldtype": "Check",
+ "label": "Allow Over Allocation"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2021-10-02 11:59:40.503359",
+ "modified": "2022-05-09 05:01:38.957545",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -251,5 +260,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 763e657..ea56d07 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -204,6 +204,29 @@
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
+
+ if (irn && !irn_cancelled) {
+ const action = () => {
+ const dialog = frappe.msgprint({
+ title: __("Generate QRCode"),
+ message: __("Generate and attach QR Code using IRN?"),
+ primary_action: {
+ action: function() {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: () => frm.reload_doc() || dialog.hide(),
+ error: () => dialog.hide()
+ });
+ }
+ },
+ primary_action_label: __('Yes')
+ });
+ dialog.show();
+ };
+ add_custom_button(__("Generate QRCode"), action);
+ }
}
});
};
@@ -211,86 +234,100 @@
const get_ewaybill_fields = (frm) => {
return [
{
- 'fieldname': 'transporter',
- 'label': 'Transporter',
- 'fieldtype': 'Link',
- 'options': 'Supplier',
- 'default': frm.doc.transporter
+ fieldname: "eway_part_a_section_break",
+ fieldtype: "Section Break",
+ label: "Part A",
},
{
- 'fieldname': 'gst_transporter_id',
- 'label': 'GST Transporter ID',
- 'fieldtype': 'Data',
- 'default': frm.doc.gst_transporter_id
+ fieldname: "transporter",
+ label: "Transporter",
+ fieldtype: "Link",
+ options: "Supplier",
+ default: frm.doc.transporter,
},
{
- 'fieldname': 'driver',
- 'label': 'Driver',
- 'fieldtype': 'Link',
- 'options': 'Driver',
- 'default': frm.doc.driver
+ fieldname: "transporter_name",
+ label: "Transporter Name",
+ fieldtype: "Data",
+ read_only: 1,
+ default: frm.doc.transporter_name,
+ depends_on: "transporter",
},
{
- 'fieldname': 'lr_no',
- 'label': 'Transport Receipt No',
- 'fieldtype': 'Data',
- 'default': frm.doc.lr_no
+ fieldname: "part_a_column_break",
+ fieldtype: "Column Break",
},
{
- 'fieldname': 'vehicle_no',
- 'label': 'Vehicle No',
- 'fieldtype': 'Data',
- 'default': frm.doc.vehicle_no
+ fieldname: "gst_transporter_id",
+ label: "GST Transporter ID",
+ fieldtype: "Data",
+ default: frm.doc.gst_transporter_id,
},
{
- 'fieldname': 'distance',
- 'label': 'Distance (in km)',
- 'fieldtype': 'Float',
- 'default': frm.doc.distance,
- 'description': 'Set as zero to auto calculate distance using pin codes',
+ fieldname: "distance",
+ label: "Distance (in km)",
+ fieldtype: "Float",
+ default: frm.doc.distance,
+ description: 'Set as zero to auto calculate distance using pin codes',
},
{
- 'fieldname': 'transporter_col_break',
- 'fieldtype': 'Column Break',
+ fieldname: "eway_part_b_section_break",
+ fieldtype: "Section Break",
+ label: "Part B",
},
{
- 'fieldname': 'transporter_name',
- 'label': 'Transporter Name',
- 'fieldtype': 'Data',
- 'read_only': 1,
- 'default': frm.doc.transporter_name,
- 'depends_on': 'transporter'
+ fieldname: "mode_of_transport",
+ label: "Mode of Transport",
+ fieldtype: "Select",
+ options: `\nRoad\nAir\nRail\nShip`,
+ default: frm.doc.mode_of_transport,
},
{
- 'fieldname': 'mode_of_transport',
- 'label': 'Mode of Transport',
- 'fieldtype': 'Select',
- 'options': `\nRoad\nAir\nRail\nShip`,
- 'default': frm.doc.mode_of_transport
+ fieldname: "gst_vehicle_type",
+ label: "GST Vehicle Type",
+ fieldtype: "Select",
+ options: `Regular\nOver Dimensional Cargo (ODC)`,
+ depends_on: 'eval:(doc.mode_of_transport === "Road")',
+ default: frm.doc.gst_vehicle_type,
},
{
- 'fieldname': 'driver_name',
- 'label': 'Driver Name',
- 'fieldtype': 'Data',
- 'fetch_from': 'driver.full_name',
- 'read_only': 1,
- 'default': frm.doc.driver_name,
- 'depends_on': 'driver'
+ fieldname: "vehicle_no",
+ label: "Vehicle No",
+ fieldtype: "Data",
+ default: frm.doc.vehicle_no,
},
{
- 'fieldname': 'lr_date',
- 'label': 'Transport Receipt Date',
- 'fieldtype': 'Date',
- 'default': frm.doc.lr_date
+ fieldname: "part_b_column_break",
+ fieldtype: "Column Break",
},
{
- 'fieldname': 'gst_vehicle_type',
- 'label': 'GST Vehicle Type',
- 'fieldtype': 'Select',
- 'options': `Regular\nOver Dimensional Cargo (ODC)`,
- 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
- 'default': frm.doc.gst_vehicle_type
- }
+ fieldname: "lr_date",
+ label: "Transport Receipt Date",
+ fieldtype: "Date",
+ default: frm.doc.lr_date,
+ },
+ {
+ fieldname: "lr_no",
+ label: "Transport Receipt No",
+ fieldtype: "Data",
+ default: frm.doc.lr_no,
+ },
+ {
+ fieldname: "driver",
+ label: "Driver",
+ fieldtype: "Link",
+ options: "Driver",
+ default: frm.doc.driver,
+ },
+ {
+ fieldname: "driver_name",
+ label: "Driver Name",
+ fieldtype: "Data",
+ fetch_from: "driver.full_name",
+ read_only: 1,
+ default: frm.doc.driver_name,
+ depends_on: "driver",
+ },
];
};
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 53d3211..417f883 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -794,6 +794,7 @@
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
+ self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
def set_invoice(self):
self.invoice = None
@@ -857,8 +858,8 @@
return res
def auto_refresh_token(self):
- self.fetch_auth_token()
self.token_auto_refreshed = True
+ self.fetch_auth_token()
def log_request(self, url, headers, data, res):
headers.update({"password": self.credentials.password})
@@ -998,6 +999,37 @@
return failed
+ def fetch_and_attach_qrcode_from_irn(self):
+ qrcode = self.get_qrcode_from_irn(self.invoice.irn)
+ if qrcode:
+ qrcode_file = self.create_qr_code_file(qrcode)
+ frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
+ frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
+ else:
+ frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
+
+ def get_qrcode_from_irn(self, irn):
+ import requests
+
+ headers = self.get_headers()
+ headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn})
+
+ try:
+ # using requests.get instead of make_request to avoid parsing the response
+ res = requests.get(self.get_qrcode_url, headers=headers)
+ self.log_request(self.get_qrcode_url, headers, None, None)
+ if res.status_code == 200:
+ return res.content
+ else:
+ raise RequestFailed(str(res.content, "utf-8"))
+
+ except RequestFailed as e:
+ self.raise_error(errors=str(e))
+
+ except Exception:
+ log_error()
+ self.raise_error()
+
def get_irn_details(self, irn):
headers = self.get_headers()
@@ -1198,8 +1230,6 @@
return errors
def raise_error(self, raise_exception=False, errors=None):
- if errors is None:
- errors = []
title = _("E Invoice Request Failed")
if errors:
frappe.throw(errors, title=title, as_list=1)
@@ -1240,13 +1270,18 @@
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
- doctype = self.invoice.doctype
- docname = self.invoice.name
- filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
qr_image = io.BytesIO()
url = qrcreate(qrcode, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
+ qrcode_file = self.create_qr_code_file(qr_image.getvalue())
+ self.invoice.qrcode_image = qrcode_file.file_url
+
+ def create_qr_code_file(self, qr_image):
+ doctype = self.invoice.doctype
+ docname = self.invoice.name
+ filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
+
_file = frappe.get_doc(
{
"doctype": "File",
@@ -1255,12 +1290,12 @@
"attached_to_name": docname,
"attached_to_field": "qrcode_image",
"is_private": 0,
- "content": qr_image.getvalue(),
+ "content": qr_image,
}
)
_file.save()
frappe.db.commit()
- self.invoice.qrcode_image = _file.file_url
+ return _file
def update_invoice(self):
self.invoice.flags.ignore_validate_update_after_submit = True
@@ -1306,6 +1341,12 @@
@frappe.whitelist()
+def generate_qrcode(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.fetch_and_attach_qrcode_from_irn()
+
+
+@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_eway_bill(**kwargs)
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 7a68386..cb4bd51 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -479,16 +479,20 @@
frappe.dom.freeze();
this.frm = this.get_new_frm(this.frm);
this.frm.doc.items = [];
- const res = await frappe.call({
+ return frappe.call({
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
args: {
'source_name': doc.name,
'target_doc': this.frm.doc
+ },
+ callback: (r) => {
+ frappe.model.sync(r.message);
+ frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
+ this.set_pos_profile_data().then(() => {
+ frappe.dom.unfreeze();
+ });
}
});
- frappe.model.sync(res.message);
- await this.set_pos_profile_data();
- frappe.dom.unfreeze();
}
set_pos_profile_data() {
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 32c58c5..b8f4803 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -16,6 +16,9 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
+ EmptyStockReconciliationItemsError,
+)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
@@ -180,9 +183,12 @@
if not frappe.db.exists("Item", item_code):
create_item(item_code)
- create_stock_reconciliation(
- item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
- )
+ try:
+ create_stock_reconciliation(
+ item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
+ )
+ except EmptyStockReconciliationItemsError:
+ pass
if frappe.db.exists("Item", "Test FG A RW 1"):
doc = frappe.get_doc("Item", "Test FG A RW 1")
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 3ccd342..b9c57c1 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -652,6 +652,104 @@
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
+ def test_serial_batch_item_stock_entry(self):
+ """
+ Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
+ 2) Cancel same Stock Entry
+ Expected Result: 1) Batch is created with Reference in Serial No
+ 2) Batch is deleted and Serial No is Inactive
+ """
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
+
+ se = make_stock_entry(
+ item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
+ )
+ batch_no = se.items[0].batch_no
+ serial_no = get_serial_nos(se.items[0].serial_no)[0]
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+
+ batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
+ self.assertEqual(batch_in_serial_no, batch_no)
+
+ self.assertEqual(batch_qty, 1)
+
+ se.cancel()
+
+ batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
+ self.assertEqual(batch_in_serial_no, None)
+
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
+ self.assertEqual(frappe.db.exists("Batch", batch_no), None)
+
+ def test_serial_batch_item_qty_deduction(self):
+ """
+ Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
+ Expected: 1) Cancelling first Stock Entry (origin transaction of created batch)
+ should throw a LinkExistsError
+ 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
+ and in that transaction only, Inactive.
+ """
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
+
+ se1 = make_stock_entry(
+ item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
+ )
+ batch_no = se1.items[0].batch_no
+ serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
+
+ # Check Source (Origin) Document of Batch
+ self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
+
+ se2 = make_stock_entry(
+ item_code=item.item_code,
+ target="_Test Warehouse - _TC",
+ qty=1,
+ basic_rate=100,
+ batch_no=batch_no,
+ )
+ serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
+
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+ self.assertEqual(batch_qty, 2)
+
+ se2.cancel()
+
+ # Check decrease in Batch Qty
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+ self.assertEqual(batch_qty, 1)
+
+ # Check if Serial No from Stock Entry 1 is intact
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
+
+ # Check if Serial No from Stock Entry 2 is Unlinked and Inactive
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
+
def test_warehouse_company_validation(self):
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
frappe.get_doc("User", "test2@example.com").add_roles(
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 5d5a27f..bd60cf0 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -62,6 +62,7 @@
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
+ self.delete_auto_created_batches()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
@@ -456,7 +457,7 @@
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
- d.total_amount = d.actual_qty * d.valuation_rate
+ d.total_amount = flt(d.actual_qty) * d.valuation_rate
merge_similar_entries[key] = d
elif d.serial_no:
data = merge_similar_entries[key]
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 1e59aae..e7b89b1 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -250,7 +250,7 @@
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(
- item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
+ item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1
)
sr.save()
sr.submit()
@@ -288,6 +288,84 @@
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
+ def test_stock_reco_for_serial_and_batch_item(self):
+ item = create_item("_TestBatchSerialItemReco")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "TBS-BATCH-.##"
+ item.serial_no_series = "TBS-.####"
+ item.save()
+
+ warehouse = "_Test Warehouse for Stock Reco2 - _TC"
+
+ sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100)
+
+ batch_no = sr.items[0].batch_no
+
+ serial_nos = get_serial_nos(sr.items[0].serial_no)
+ self.assertEqual(len(serial_nos), 1)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
+
+ sr.cancel()
+
+ self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
+ self.assertEqual(frappe.db.exists("Batch", batch_no), None)
+
+ def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
+ """
+ Behaviour: 1) Create Stock Reconciliation, which will be the origin document
+ of a new batch having a serial no
+ 2) Create a Stock Entry that adds a serial no to the same batch following this
+ Stock Reconciliation
+ 3) Cancel Stock Entry
+ Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases
+ """
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+ item = create_item("_TestBatchSerialItemDependentReco")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "TBSD-BATCH-.##"
+ item.serial_no_series = "TBSD-.####"
+ item.save()
+
+ warehouse = "_Test Warehouse for Stock Reco2 - _TC"
+
+ stock_reco = create_stock_reconciliation(
+ item_code=item.item_code, warehouse=warehouse, qty=1, rate=100
+ )
+ batch_no = stock_reco.items[0].batch_no
+ reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
+
+ stock_entry = make_stock_entry(
+ item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no
+ )
+ serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
+
+ # Check Batch qty after 2 transactions
+ batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
+ self.assertEqual(batch_qty, 2)
+
+ # Cancel latest stock document
+ stock_entry.cancel()
+
+ # Check Batch qty after cancellation
+ batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
+ self.assertEqual(batch_qty, 1)
+
+ # Check if Serial No from Stock Reconcilation is intact
+ self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no)
+ self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active")
+
+ # Check if Serial No from Stock Entry is Unlinked and Inactive
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
+
+ stock_reco.cancel()
+
def test_customer_provided_items(self):
item_code = "Stock-Reco-customer-Item-100"
create_item(
@@ -684,11 +762,13 @@
},
)
- try:
- if not args.do_not_submit:
- sr.submit()
- except EmptyStockReconciliationItemsError:
- pass
+ if not args.do_not_save:
+ sr.insert()
+ try:
+ if not args.do_not_submit:
+ sr.submit()
+ except EmptyStockReconciliationItemsError:
+ pass
return sr