Merge branch 'develop' into actual-qty-total-js-reactive
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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 78645e0..46013bb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2451,11 +2451,21 @@
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
)
- def validate_quantity(child_item, d):
- if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
+ def validate_quantity(child_item, new_data):
+ if not flt(new_data.get("qty")):
+ frappe.throw(
+ _("Row # {0}: Quantity for Item {1} cannot be zero").format(
+ new_data.get("idx"), frappe.bold(new_data.get("item_code"))
+ ),
+ title=_("Invalid Qty"),
+ )
+
+ if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
- if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
+ if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
+ child_item.received_qty
+ ):
frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items)
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 19b4d68..b590177 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -9,7 +9,7 @@
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
-from frappe.utils import cint, cstr, flt, get_fullname
+from frappe.utils import cint, flt, get_fullname
from erpnext.crm.utils import add_link_in_communication, copy_comments
from erpnext.setup.utils import get_exchange_rate
@@ -215,20 +215,20 @@
if self.party_name and self.opportunity_from == "Customer":
if self.contact_person:
- opts.description = "Contact " + cstr(self.contact_person)
+ opts.description = f"Contact {self.contact_person}"
else:
- opts.description = "Contact customer " + cstr(self.party_name)
+ opts.description = f"Contact customer {self.party_name}"
elif self.party_name and self.opportunity_from == "Lead":
if self.contact_display:
- opts.description = "Contact " + cstr(self.contact_display)
+ opts.description = f"Contact {self.contact_display}"
else:
- opts.description = "Contact lead " + cstr(self.party_name)
+ opts.description = f"Contact lead {self.party_name}"
opts.subject = opts.description
- opts.description += ". By : " + cstr(self.contact_by)
+ opts.description += f". By : {self.contact_by}"
if self.to_discuss:
- opts.description += " To Discuss : " + cstr(self.to_discuss)
+ opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
super(Opportunity, self).add_calendar_event(opts, force)
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py
index 481c7f1..4a18e94 100644
--- a/erpnext/crm/doctype/opportunity/test_opportunity.py
+++ b/erpnext/crm/doctype/opportunity/test_opportunity.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import now_datetime, random_string, today
+from frappe.utils import add_days, now_datetime, random_string, today
from erpnext.crm.doctype.lead.lead import make_customer
from erpnext.crm.doctype.lead.test_lead import make_lead
@@ -97,6 +97,22 @@
self.assertEqual(quotation_comment_count, 4)
self.assertEqual(quotation_communication_count, 4)
+ def test_render_template_for_to_discuss(self):
+ doc = make_opportunity(with_items=0, opportunity_from="Lead")
+ doc.contact_by = "test@example.com"
+ doc.contact_date = add_days(today(), days=2)
+ doc.to_discuss = "{{ doc.name }} test data"
+ doc.save()
+
+ event = frappe.get_all(
+ "Event Participants",
+ fields=["parent"],
+ filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
+ )
+
+ event_description = frappe.db.get_value("Event", event[0].parent, "description")
+ self.assertTrue(doc.name in event_description)
+
def make_opportunity_from_lead():
new_lead_email_id = "new{}@example.com".format(random_string(5))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d5b1592..1fef240 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -369,4 +369,5 @@
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
-erpnext.patches.v13_0.create_accounting_dimensions_in_orders
\ No newline at end of file
+erpnext.patches.v13_0.create_accounting_dimensions_in_orders
+erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
new file mode 100644
index 0000000..a4d7012
--- /dev/null
+++ b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute():
+ dn = frappe.qb.DocType("Delivery Note")
+ dn_item = frappe.qb.DocType("Delivery Note Item")
+
+ dn_list = (
+ frappe.qb.from_(dn)
+ .inner_join(dn_item)
+ .on(dn.name == dn_item.parent)
+ .select(dn.name)
+ .where(dn.docstatus == 1)
+ .where(dn.is_return == 1)
+ .where(dn.per_billed < 100)
+ .where(dn_item.returned_qty > 0)
+ .run(as_dict=True)
+ )
+
+ frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
+ dn_item.returned_qty, 0
+ ).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
+
+ for d in dn_list:
+ dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
+ dn_doc.run_method("update_billing_status")
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index c4b27a5..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);
+ }
}
});
};
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 53d3211..ed1002a 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -167,7 +167,12 @@
title=_("Not Allowed"),
)
- invoice_type = "CRN" if invoice.is_return else "INV"
+ if invoice.is_return:
+ invoice_type = "CRN"
+ elif invoice.is_debit_note:
+ invoice_type = "DBN"
+ else:
+ invoice_type = "INV"
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
@@ -794,6 +799,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 +863,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 +1004,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 +1235,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 +1275,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 +1295,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 +1346,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/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 091c20c..e10df2a 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -238,4 +238,5 @@
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
},
"type": "bar",
+ "fieldtype": "Currency",
}
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py
index 4e0758d..4d71ce7 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.py
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.py
@@ -54,4 +54,5 @@
},
"type": "line",
"lineOptions": {"regionFill": 1},
+ "fieldtype": "Currency",
}
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index 1a2476a..9d7d806 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -415,3 +415,8 @@
else:
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
+
+ if self.filters["value_quantity"] == "Value":
+ self.chart["fieldtype"] = "Currency"
+ else:
+ self.chart["fieldtype"] = "Float"
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
index 719f1c5..18f448c 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
@@ -51,4 +51,5 @@
},
"type": "line",
"lineOptions": {"regionFill": 1},
+ "fieldtype": "Currency",
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index f97e7ca..0738bfb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -962,6 +962,44 @@
automatically_fetch_payment_terms(enable=0)
+ def test_returned_qty_in_return_dn(self):
+ # SO ---> SI ---> DN
+ # |
+ # |---> DN(Partial Sales Return) ---> SI(Credit Note)
+ # |
+ # |---> DN(Partial Sales Return) ---> SI(Credit Note)
+
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
+ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
+ so = make_sales_order(qty=10)
+ si = make_sales_invoice(so.name)
+ si.insert()
+ si.submit()
+ dn = make_delivery_note(si.name)
+ dn.insert()
+ dn.submit()
+ self.assertEqual(dn.items[0].returned_qty, 0)
+ self.assertEqual(dn.per_billed, 100)
+
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+ dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
+ si1 = make_sales_invoice(dn1.name)
+ si1.insert()
+ si1.submit()
+ dn1.reload()
+ self.assertEqual(dn1.items[0].returned_qty, 0)
+ self.assertEqual(dn1.per_billed, 100)
+
+ dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
+ si2 = make_sales_invoice(dn2.name)
+ si2.insert()
+ si2.submit()
+ dn2.reload()
+ self.assertEqual(dn2.items[0].returned_qty, 0)
+ self.assertEqual(dn2.per_billed, 100)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index e2eb2a4..2d7abc8 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -737,7 +737,9 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
- "label": "Returned Qty in Stock UOM"
+ "label": "Returned Qty in Stock UOM",
+ "no_copy": 1,
+ "read_only": 1
},
{
"fieldname": "incoming_rate",
@@ -778,7 +780,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-03-31 18:36:24.671913",
+ "modified": "2022-05-02 12:09:39.610075",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 5850ec7..4e2fc83 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -1183,6 +1183,42 @@
backdated.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
+ def test_timestamp_clash(self):
+
+ item = make_item().name
+ warehouse = "_Test Warehouse - _TC"
+
+ reciept = make_stock_entry(
+ item_code=item,
+ to_warehouse=warehouse,
+ qty=100,
+ rate=10,
+ posting_date="2021-01-01",
+ posting_time="01:00:00",
+ )
+
+ consumption = make_stock_entry(
+ item_code=item,
+ from_warehouse=warehouse,
+ qty=50,
+ posting_date="2021-01-01",
+ posting_time="02:00:00.1234", # ms are possible when submitted without editing posting time
+ )
+
+ backdated_receipt = make_stock_entry(
+ item_code=item,
+ to_warehouse=warehouse,
+ qty=100,
+ posting_date="2021-01-01",
+ rate=10,
+ posting_time="02:00:00", # same posting time as consumption but ms part stripped
+ )
+
+ try:
+ backdated_receipt.cancel()
+ except Exception as e:
+ self.fail("Double processing of qty for clashing timestamp.")
+
def create_repack_entry(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index e7b89b1..9088eb8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -31,6 +31,7 @@
def tearDown(self):
frappe.local.future_sle = {}
+ frappe.flags.pop("dont_execute_stock_reposts", None)
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@@ -384,6 +385,7 @@
-------------------------------------------
Var | Doc | Qty | Balance
-------------------------------------------
+ PR5 | PR | 10 | 10 (posting date: today-4) [backdated]
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
PR1 | PR | 10 | 18 (posting date: today-3)
PR2 | PR | 1 | 19 (posting date: today-2)
@@ -393,6 +395,14 @@
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
+ frappe.flags.dont_execute_stock_reposts = True
+
+ def assertBalance(doc, qty_after_transaction):
+ sle_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ self.assertEqual(sle_balance, qty_after_transaction)
+
pr1 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
)
@@ -402,62 +412,37 @@
pr3 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
)
-
- pr1_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- pr3_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- self.assertEqual(pr1_balance, 10)
- self.assertEqual(pr3_balance, 12)
+ assertBalance(pr1, 10)
+ assertBalance(pr3, 12)
# post backdated stock reco in between
sr4 = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
)
- pr3_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- self.assertEqual(pr3_balance, 7)
+ assertBalance(pr3, 7)
# post backdated stock reco at the start
sr5 = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
)
- pr1_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+ assertBalance(pr1, 18)
+ assertBalance(pr2, 19)
+ assertBalance(sr4, 6) # check if future stock reco is unaffected
+
+ # Make a backdated receipt and check only entries till first SR are affected
+ pr5 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5)
)
- pr2_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- sr4_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- self.assertEqual(pr1_balance, 18)
- self.assertEqual(pr2_balance, 19)
- self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+ assertBalance(pr5, 10)
+ # check if future stock reco is unaffected
+ assertBalance(sr4, 6)
+ assertBalance(sr5, 8)
# cancel backdated stock reco and check future impact
sr5.cancel()
- pr1_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- pr2_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- sr4_balance = frappe.db.get_value(
- "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
- )
- self.assertEqual(pr1_balance, 10)
- self.assertEqual(pr2_balance, 11)
- self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
-
- # teardown
- sr4.cancel()
- pr3.cancel()
- pr2.cancel()
- pr1.cancel()
+ assertBalance(pr1, 10)
+ assertBalance(pr2, 11)
+ assertBalance(sr4, 6) # check if future stock reco is unaffected
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_backdated_stock_reco_future_negative_stock(self):
@@ -563,7 +548,6 @@
# repost will make this test useless, qty should update in realtime without reposts
frappe.flags.dont_execute_stock_reposts = True
- self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
new file mode 100644
index 0000000..0b8f496
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+ "fifo_qty_diff",
+ "fifo_value_diff",
+];
+
+frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
+ "filters": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ get_query: function() {
+ return {
+ filters: {is_stock_item: 1, has_serial_no: 0}
+ }
+ }
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Posting Date",
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "From Posting Date",
+ }
+ ],
+ formatter (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+ value = "<span style='color:red'>" + value + "</span>";
+ }
+ return value;
+ },
+};
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
new file mode 100644
index 0000000..5e958aa
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2022-05-11 04:09:13.460652",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "abc",
+ "modified": "2022-05-11 04:09:20.232177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "FIFO Queue vs Qty After Transaction Comparison",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "FIFO Queue vs Qty After Transaction Comparison",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Administrator"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
new file mode 100644
index 0000000..9e14033
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+from frappe.utils.nestedset import get_descendants_of
+
+SLE_FIELDS = (
+ "name",
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "creation",
+ "voucher_type",
+ "voucher_no",
+ "actual_qty",
+ "qty_after_transaction",
+ "stock_queue",
+ "batch_no",
+ "stock_value",
+ "valuation_rate",
+)
+
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+
+def get_data(filters):
+ if not any([filters.warehouse, filters.item_code, filters.item_group]):
+ frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
+ sles = get_stock_ledger_entries(filters)
+ return find_first_bad_queue(sles)
+
+
+def get_stock_ledger_entries(filters):
+
+ sle_filters = {"is_cancelled": 0}
+
+ if filters.warehouse:
+ children = get_descendants_of("Warehouse", filters.warehouse)
+ sle_filters["warehouse"] = ("in", children + [filters.warehouse])
+
+ if filters.item_code:
+ sle_filters["item_code"] = filters.item_code
+ elif filters.get("item_group"):
+ item_group = filters.get("item_group")
+ children = get_descendants_of("Item Group", item_group)
+ item_group_filter = {"item_group": ("in", children + [item_group])}
+ sle_filters["item_code"] = (
+ "in",
+ frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
+ )
+
+ if filters.from_date:
+ sle_filters["posting_date"] = (">=", filters.from_date)
+ if filters.to_date:
+ sle_filters["posting_date"] = ("<=", filters.to_date)
+
+ return frappe.get_all(
+ "Stock Ledger Entry",
+ fields=SLE_FIELDS,
+ filters=sle_filters,
+ order_by="timestamp(posting_date, posting_time), creation",
+ )
+
+
+def find_first_bad_queue(sles):
+ item_warehouse_sles = {}
+ for sle in sles:
+ item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
+
+ data = []
+
+ for _item_wh, sles in item_warehouse_sles.items():
+ for idx, sle in enumerate(sles):
+ queue = json.loads(sle.stock_queue or "[]")
+
+ sle.fifo_queue_qty = 0.0
+ sle.fifo_stock_value = 0.0
+ for qty, rate in queue:
+ sle.fifo_queue_qty += flt(qty)
+ sle.fifo_stock_value += flt(qty) * flt(rate)
+
+ sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
+ sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
+
+ if sle.batch_no:
+ sle.use_batchwise_valuation = frappe.db.get_value(
+ "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
+ )
+
+ if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
+ if idx:
+ data.append(sles[idx - 1])
+ data.append(sle)
+ data.append({})
+ break
+
+ return data
+
+
+def get_columns():
+ return [
+ {
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "label": _("Stock Ledger Entry"),
+ "options": "Stock Ledger Entry",
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": _("Item Code"),
+ "options": "Item",
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": _("Warehouse"),
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Data",
+ "label": _("Posting Date"),
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Data",
+ "label": _("Posting Time"),
+ },
+ {
+ "fieldname": "creation",
+ "fieldtype": "Data",
+ "label": _("Creation"),
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": _("Voucher Type"),
+ "options": "DocType",
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": _("Voucher No"),
+ "options": "voucher_type",
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": _("Batch"),
+ "options": "Batch",
+ },
+ {
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": _("Batchwise Valuation"),
+ },
+ {
+ "fieldname": "actual_qty",
+ "fieldtype": "Float",
+ "label": _("Qty Change"),
+ },
+ {
+ "fieldname": "qty_after_transaction",
+ "fieldtype": "Float",
+ "label": _("(A) Qty After Transaction"),
+ },
+ {
+ "fieldname": "stock_queue",
+ "fieldtype": "Data",
+ "label": _("FIFO/LIFO Queue"),
+ },
+ {
+ "fieldname": "fifo_queue_qty",
+ "fieldtype": "Float",
+ "label": _("(C) Total qty in queue"),
+ },
+ {
+ "fieldname": "fifo_qty_diff",
+ "fieldtype": "Float",
+ "label": _("A - C"),
+ },
+ {
+ "fieldname": "stock_value",
+ "fieldtype": "Float",
+ "label": _("(D) Balance Stock Value"),
+ },
+ {
+ "fieldname": "fifo_stock_value",
+ "fieldtype": "Float",
+ "label": _("(E) Balance Stock Value in Queue"),
+ },
+ {
+ "fieldname": "fifo_value_diff",
+ "fieldtype": "Float",
+ "label": _("D - E"),
+ },
+ {
+ "fieldname": "valuation_rate",
+ "fieldtype": "Float",
+ "label": _("(H) Valuation Rate"),
+ },
+ ]
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index 837c4a6..ed0e2fc 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -111,17 +111,17 @@
},
{
"fieldname": "posting_date",
- "fieldtype": "Date",
+ "fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
- "fieldtype": "Time",
+ "fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
- "fieldtype": "Datetime",
+ "fieldtype": "Data",
"label": _("Creation"),
},
{
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 55b9104..d118d8e 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -65,6 +65,8 @@
("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
+ ("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
+ ("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
]
OPTIONAL_FILTERS = {
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 7e5c231..4789b52 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1303,6 +1303,8 @@
datetime_limit_condition = ""
qty_shift = args.actual_qty
+ args["time_format"] = "%H:%i:%s"
+
# find difference/shift in qty caused by stock reconciliation
if args.voucher_type == "Stock Reconciliation":
qty_shift = get_stock_reco_qty_shift(args)
@@ -1315,7 +1317,7 @@
datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql(
- """
+ f"""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty_shift}
where
@@ -1323,16 +1325,10 @@
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
- and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
- or (
- timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
- and creation > %(creation)s
- )
- )
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s))
+ > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
{datetime_limit_condition}
- """.format(
- qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
- ),
+ """,
args,
)
@@ -1383,6 +1379,7 @@
and creation > %(creation)s
)
)
+ order by timestamp(posting_date, posting_time) asc, creation asc
limit 1
""",
args,
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 6ca3344..fb56ff6 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -1357,7 +1357,7 @@
"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Цена товара отображается несколько раз на основе Прайс-листа, Поставщика / Клиента, Валюты, Предмет, UOM, Кол-во и Даты.",
Item Price updated for {0} in Price List {1},Цена продукта {0} обновлена в прайс-листе {1},
Item Row {0}: {1} {2} does not exist in above '{1}' table,Элемент Row {0}: {1} {2} не существует в таблице «{1}»,
-Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом,"
+Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом",
Item Template,Шаблон продукта,
Item Variant Settings,Параметры модификации продкута,
Item Variant {0} already exists with same attributes,Модификация продукта {0} с этими атрибутами уже существует,