Merge branch 'develop' into actual-qty-total-js-reactive
This commit is contained in:
commit
d024f72ab8
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
@ -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")
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -238,4 +238,5 @@ def get_chart_data(data):
|
||||
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
|
||||
},
|
||||
"type": "bar",
|
||||
"fieldtype": "Currency",
|
||||
}
|
||||
|
@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
|
||||
},
|
||||
"type": "line",
|
||||
"lineOptions": {"regionFill": 1},
|
||||
"fieldtype": "Currency",
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -51,4 +51,5 @@ def get_chart_data(data, conditions, filters):
|
||||
},
|
||||
"type": "line",
|
||||
"lineOptions": {"regionFill": 1},
|
||||
"fieldtype": "Currency",
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"),
|
||||
},
|
||||
]
|
@ -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"),
|
||||
},
|
||||
{
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user