Merge branch 'develop' into actual-qty-total-js-reactive

This commit is contained in:
Marica 2022-05-11 18:02:20 +05:30 committed by GitHub
commit d024f72ab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 565 additions and 80 deletions

View File

@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
# 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):

View File

@ -2451,11 +2451,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
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)

View File

@ -9,7 +9,7 @@ from frappe import _
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 @@ class Opportunity(TransactionBase):
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)

View File

@ -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 @@ class TestOpportunity(unittest.TestCase):
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))

View File

@ -369,4 +369,5 @@ erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
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
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note

View File

@ -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")

View File

@ -204,6 +204,29 @@ erpnext.setup_einvoice_actions = (doctype) => {
};
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);
}
}
});
};

View File

@ -167,7 +167,12 @@ def get_doc_details(invoice):
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 @@ class GSPConnector:
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 @@ class GSPConnector:
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 @@ class GSPConnector:
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 @@ class GSPConnector:
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 @@ class GSPConnector:
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 @@ class GSPConnector:
"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
@ -1305,6 +1345,12 @@ def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector.cancel_irn(irn, reason, remark)
@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)

View File

@ -238,4 +238,5 @@ def get_chart_data(data):
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
},
"type": "bar",
"fieldtype": "Currency",
}

View File

@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
},
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
}

View File

@ -415,3 +415,8 @@ class Analytics(object):
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"

View File

@ -51,4 +51,5 @@ def get_chart_data(data, conditions, filters):
},
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
}

View File

@ -962,6 +962,44 @@ class TestDeliveryNote(FrappeTestCase):
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")

View File

@ -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",

View File

@ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase):
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)

View File

@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
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 @@ class TestStockReconciliation(FrappeTestCase):
-------------------------------------------
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 @@ class TestStockReconciliation(FrappeTestCase):
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 @@ class TestStockReconciliation(FrappeTestCase):
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 @@ class TestStockReconciliation(FrappeTestCase):
# 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"

View File

@ -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;
},
};

View File

@ -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"
}
]
}

View File

@ -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"),
},
]

View File

@ -111,17 +111,17 @@ def get_columns():
},
{
"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"),
},
{

View File

@ -65,6 +65,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("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 = {

View File

@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
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 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
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 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
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 @@ def get_next_stock_reco(args):
and creation > %(creation)s
)
)
order by timestamp(posting_date, posting_time) asc, creation asc
limit 1
""",
args,

View File

@ -1357,7 +1357,7 @@ Item Price added for {0} in Price List {1},Цена продукта {0} доб
"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} с этими атрибутами уже существует,

Can't render this file because it is too large.