Merge branch 'develop' into de-translate-employee
This commit is contained in:
commit
195e8af985
@ -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):
|
||||
|
@ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None):
|
||||
def get_tax_template(posting_date, args):
|
||||
"""Get matching tax rule"""
|
||||
args = frappe._dict(args)
|
||||
from_date = to_date = posting_date
|
||||
if not posting_date:
|
||||
from_date = "1900-01-01"
|
||||
to_date = "4000-01-01"
|
||||
conditions = []
|
||||
|
||||
conditions = [
|
||||
"""(from_date is null or from_date <= '{0}')
|
||||
and (to_date is null or to_date >= '{1}')""".format(
|
||||
from_date, to_date
|
||||
if posting_date:
|
||||
conditions.append(
|
||||
f"""(from_date is null or from_date <= '{posting_date}')
|
||||
and (to_date is null or to_date >= '{posting_date}')"""
|
||||
)
|
||||
]
|
||||
else:
|
||||
conditions.append("(from_date is null) and (to_date is null)")
|
||||
|
||||
conditions.append(
|
||||
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
||||
|
@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
|
||||
"""
|
||||
SELECT
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
||||
p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount,
|
||||
p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
|
||||
p.customer, p.is_return {select_mop_field}
|
||||
FROM
|
||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||
|
@ -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))
|
||||
|
@ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
tax_rule_master = set_taxes(
|
||||
quotation.party_name,
|
||||
"Customer",
|
||||
quotation.transaction_date,
|
||||
None,
|
||||
quotation.company,
|
||||
customer_group=None,
|
||||
supplier_group=None,
|
||||
|
@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
|
||||
|
||||
develop_version = "13.x.x-develop"
|
||||
develop_version = "14.x.x-develop"
|
||||
|
||||
app_include_js = "erpnext.bundle.js"
|
||||
app_include_css = "erpnext.bundle.css"
|
||||
|
@ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// make new leaves allocated field read only if allocation is created via leave policy assignment
|
||||
// and leave type is earned leave, since these leaves would be allocated via the scheduler
|
||||
if (frm.doc.leave_policy_assignment) {
|
||||
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
|
||||
if (r && cint(r.is_earned_leave))
|
||||
frm.set_df_property("new_leaves_allocated", "read_only", 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
expire_allocation: function(frm) {
|
||||
|
@ -254,7 +254,18 @@ class LeaveAllocation(Document):
|
||||
# Adding a day to include To Date in the difference
|
||||
date_difference = date_diff(self.to_date, self.from_date) + 1
|
||||
if date_difference < self.total_leaves_allocated:
|
||||
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"):
|
||||
frappe.msgprint(
|
||||
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
|
||||
exc=OverAllocationError,
|
||||
title=_("Over Allocation"),
|
||||
)
|
||||
|
||||
def create_leave_ledger_entry(self, submit=True):
|
||||
if self.unused_leaves:
|
||||
|
@ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_validation_for_over_allocation(self):
|
||||
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
|
||||
leave_type.save()
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"leave_type": leave_type.name,
|
||||
"from_date": getdate("2015-09-1"),
|
||||
"to_date": getdate("2015-09-30"),
|
||||
"new_leaves_allocated": 35,
|
||||
"carry_forward": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# allocated leave more than period
|
||||
self.assertRaises(OverAllocationError, doc.save)
|
||||
|
||||
leave_type.allow_over_allocation = 1
|
||||
leave_type.save()
|
||||
|
||||
# allows creating a leave allocation with more leave days than period days
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": leave_type.name,
|
||||
"from_date": getdate("2015-09-1"),
|
||||
"to_date": getdate("2015-09-30"),
|
||||
"new_leaves_allocated": 35,
|
||||
"carry_forward": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
def test_validation_for_over_allocation_post_submission(self):
|
||||
allocation = frappe.get_doc(
|
||||
{
|
||||
|
@ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
|
||||
i = 0
|
||||
while i < 14:
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
|
||||
@ -753,7 +753,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||
i = 0
|
||||
while i < 6:
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
@ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
test_dependencies = ["Employee"]
|
||||
|
||||
|
||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
class TestLeavePolicyAssignment(FrappeTestCase):
|
||||
def setUp(self):
|
||||
for doctype in [
|
||||
"Leave Period",
|
||||
@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
leave_policy = create_leave_policy()
|
||||
leave_policy.submit()
|
||||
|
||||
self.employee.date_of_joining = get_first_day(leave_period.from_date)
|
||||
self.employee.save()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
@ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
|
||||
@ -242,20 +233,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
self.assertEqual(details.unused_leaves, 5)
|
||||
self.assertEqual(details.total_leaves_allocated, 7)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import is_earned_leave_already_allocated
|
||||
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
|
||||
allocation = frappe.get_doc("Leave Allocation", details.name)
|
||||
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
|
||||
self.assertFalse(
|
||||
is_earned_leave_already_allocated(
|
||||
allocation, leave_policy.leave_policy_details[0].annual_allocation
|
||||
)
|
||||
)
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
|
||||
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
@ -288,19 +265,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
|
||||
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
|
||||
leave_period, leave_policy = setup_leave_period_and_policy(
|
||||
@ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
|
||||
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
|
||||
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
|
||||
@ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||
frappe.flags.current_date = None
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
"fraction_of_daily_salary_per_leave",
|
||||
"is_optional_leave",
|
||||
"allow_negative",
|
||||
"allow_over_allocation",
|
||||
"include_holiday",
|
||||
"is_compensatory",
|
||||
"carry_forward_section",
|
||||
@ -211,15 +212,23 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Fraction of Daily Salary per Leave",
|
||||
"mandatory_depends_on": "eval:doc.is_ppl == 1"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows allocating more leaves than the number of days in the allocation period.",
|
||||
"fieldname": "allow_over_allocation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Over Allocation"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-flag",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-02 11:59:40.503359",
|
||||
"modified": "2022-05-09 05:01:38.957545",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Type",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -251,5 +260,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -269,7 +269,7 @@ def generate_leave_encashment():
|
||||
create_leave_encashment(leave_allocation=leave_allocation)
|
||||
|
||||
|
||||
def allocate_earned_leaves(ignore_duplicates=False):
|
||||
def allocate_earned_leaves():
|
||||
"""Allocate earned leaves to Employees"""
|
||||
e_leave_types = get_earned_leaves()
|
||||
today = getdate()
|
||||
@ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False):
|
||||
if check_effective_date(
|
||||
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
|
||||
):
|
||||
update_previous_leave_allocation(
|
||||
allocation, annual_allocation, e_leave_type, ignore_duplicates
|
||||
)
|
||||
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
|
||||
|
||||
|
||||
def update_previous_leave_allocation(
|
||||
allocation, annual_allocation, e_leave_type, ignore_duplicates=False
|
||||
):
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||
earned_leaves = get_monthly_earned_leave(
|
||||
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
||||
)
|
||||
@ -326,20 +322,19 @@ def update_previous_leave_allocation(
|
||||
if new_allocation != allocation.total_leaves_allocated:
|
||||
today_date = today()
|
||||
|
||||
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
if e_leave_type.based_on_date_of_joining:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
else:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
if e_leave_type.based_on_date_of_joining:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
else:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
|
||||
allocation.add_comment(comment_type="Info", text=text)
|
||||
allocation.add_comment(comment_type="Info", text=text)
|
||||
|
||||
|
||||
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
|
@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
...data
|
||||
},
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() || d.hide(),
|
||||
error: () => d.hide()
|
||||
callback: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill Generated successfully'),
|
||||
indicator: 'green'
|
||||
}, 7);
|
||||
frm.reload_doc();
|
||||
d.hide();
|
||||
},
|
||||
error: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill was not Generated'),
|
||||
indicator: 'red'
|
||||
}, 7);
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
@ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
}
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
const action = () => {
|
||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Cancel E-Way Bill'),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: {
|
||||
doctype,
|
||||
docname: name,
|
||||
eway_bill: ewaybill,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill Cancelled successfully'),
|
||||
indicator: 'green'
|
||||
}, 7);
|
||||
frm.reload_doc();
|
||||
d.hide();
|
||||
},
|
||||
error: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill was not Cancelled'),
|
||||
indicator: 'red'
|
||||
}, 7);
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
}
|
||||
|
||||
if (irn && !irn_cancelled) {
|
||||
const action = () => {
|
||||
const dialog = frappe.msgprint({
|
||||
title: __('Update E-Way Bill Cancelled Status?'),
|
||||
message: message,
|
||||
indicator: 'orange',
|
||||
title: __("Generate QRCode"),
|
||||
message: __("Generate and attach QR Code using IRN?"),
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() || dialog.hide()
|
||||
callback: () => frm.reload_doc() || dialog.hide(),
|
||||
error: () => dialog.hide()
|
||||
});
|
||||
}
|
||||
},
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
dialog.show();
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
add_custom_button(__("Generate QRCode"), action);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -167,85 +234,100 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
const get_ewaybill_fields = (frm) => {
|
||||
return [
|
||||
{
|
||||
'fieldname': 'transporter',
|
||||
'label': 'Transporter',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Supplier',
|
||||
'default': frm.doc.transporter
|
||||
fieldname: "eway_part_a_section_break",
|
||||
fieldtype: "Section Break",
|
||||
label: "Part A",
|
||||
},
|
||||
{
|
||||
'fieldname': 'gst_transporter_id',
|
||||
'label': 'GST Transporter ID',
|
||||
'fieldtype': 'Data',
|
||||
'default': frm.doc.gst_transporter_id
|
||||
fieldname: "transporter",
|
||||
label: "Transporter",
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
default: frm.doc.transporter,
|
||||
},
|
||||
{
|
||||
'fieldname': 'driver',
|
||||
'label': 'Driver',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Driver',
|
||||
'default': frm.doc.driver
|
||||
fieldname: "transporter_name",
|
||||
label: "Transporter Name",
|
||||
fieldtype: "Data",
|
||||
read_only: 1,
|
||||
default: frm.doc.transporter_name,
|
||||
depends_on: "transporter",
|
||||
},
|
||||
{
|
||||
'fieldname': 'lr_no',
|
||||
'label': 'Transport Receipt No',
|
||||
'fieldtype': 'Data',
|
||||
'default': frm.doc.lr_no
|
||||
fieldname: "part_a_column_break",
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
'fieldname': 'vehicle_no',
|
||||
'label': 'Vehicle No',
|
||||
'fieldtype': 'Data',
|
||||
'default': frm.doc.vehicle_no
|
||||
fieldname: "gst_transporter_id",
|
||||
label: "GST Transporter ID",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.gst_transporter_id,
|
||||
},
|
||||
{
|
||||
'fieldname': 'distance',
|
||||
'label': 'Distance (in km)',
|
||||
'fieldtype': 'Float',
|
||||
'default': frm.doc.distance
|
||||
fieldname: "distance",
|
||||
label: "Distance (in km)",
|
||||
fieldtype: "Float",
|
||||
default: frm.doc.distance,
|
||||
description: 'Set as zero to auto calculate distance using pin codes',
|
||||
},
|
||||
{
|
||||
'fieldname': 'transporter_col_break',
|
||||
'fieldtype': 'Column Break',
|
||||
fieldname: "eway_part_b_section_break",
|
||||
fieldtype: "Section Break",
|
||||
label: "Part B",
|
||||
},
|
||||
{
|
||||
'fieldname': 'transporter_name',
|
||||
'label': 'Transporter Name',
|
||||
'fieldtype': 'Data',
|
||||
'read_only': 1,
|
||||
'default': frm.doc.transporter_name,
|
||||
'depends_on': 'transporter'
|
||||
fieldname: "mode_of_transport",
|
||||
label: "Mode of Transport",
|
||||
fieldtype: "Select",
|
||||
options: `\nRoad\nAir\nRail\nShip`,
|
||||
default: frm.doc.mode_of_transport,
|
||||
},
|
||||
{
|
||||
'fieldname': 'mode_of_transport',
|
||||
'label': 'Mode of Transport',
|
||||
'fieldtype': 'Select',
|
||||
'options': `\nRoad\nAir\nRail\nShip`,
|
||||
'default': frm.doc.mode_of_transport
|
||||
fieldname: "gst_vehicle_type",
|
||||
label: "GST Vehicle Type",
|
||||
fieldtype: "Select",
|
||||
options: `Regular\nOver Dimensional Cargo (ODC)`,
|
||||
depends_on: 'eval:(doc.mode_of_transport === "Road")',
|
||||
default: frm.doc.gst_vehicle_type,
|
||||
},
|
||||
{
|
||||
'fieldname': 'driver_name',
|
||||
'label': 'Driver Name',
|
||||
'fieldtype': 'Data',
|
||||
'fetch_from': 'driver.full_name',
|
||||
'read_only': 1,
|
||||
'default': frm.doc.driver_name,
|
||||
'depends_on': 'driver'
|
||||
fieldname: "vehicle_no",
|
||||
label: "Vehicle No",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.vehicle_no,
|
||||
},
|
||||
{
|
||||
'fieldname': 'lr_date',
|
||||
'label': 'Transport Receipt Date',
|
||||
'fieldtype': 'Date',
|
||||
'default': frm.doc.lr_date
|
||||
fieldname: "part_b_column_break",
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
'fieldname': 'gst_vehicle_type',
|
||||
'label': 'GST Vehicle Type',
|
||||
'fieldtype': 'Select',
|
||||
'options': `Regular\nOver Dimensional Cargo (ODC)`,
|
||||
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
||||
'default': frm.doc.gst_vehicle_type
|
||||
}
|
||||
fieldname: "lr_date",
|
||||
label: "Transport Receipt Date",
|
||||
fieldtype: "Date",
|
||||
default: frm.doc.lr_date,
|
||||
},
|
||||
{
|
||||
fieldname: "lr_no",
|
||||
label: "Transport Receipt No",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.lr_no,
|
||||
},
|
||||
{
|
||||
fieldname: "driver",
|
||||
label: "Driver",
|
||||
fieldtype: "Link",
|
||||
options: "Driver",
|
||||
default: frm.doc.driver,
|
||||
},
|
||||
{
|
||||
fieldname: "driver_name",
|
||||
label: "Driver Name",
|
||||
fieldtype: "Data",
|
||||
fetch_from: "driver.full_name",
|
||||
read_only: 1,
|
||||
default: frm.doc.driver_name,
|
||||
depends_on: "driver",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -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")
|
||||
@ -792,8 +797,9 @@ class GSPConnector:
|
||||
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
|
||||
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
||||
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()
|
||||
|
||||
@ -1113,6 +1150,19 @@ class GSPConnector:
|
||||
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
|
||||
self.invoice.eway_bill_cancelled = 0
|
||||
self.invoice.update(args)
|
||||
if res.get("info"):
|
||||
info = res.get("info")
|
||||
# when we have more features (responses) in eway bill, we can add them using below forloop.
|
||||
for msg in info:
|
||||
if msg.get("InfCd") == "EWBPPD":
|
||||
pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group())
|
||||
frappe.msgprint(
|
||||
_("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)),
|
||||
title="Notification",
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
self.invoice.distance = flt(pin_to_pin_distance)
|
||||
self.invoice.flags.updater_reference = {
|
||||
"doctype": self.invoice.doctype,
|
||||
"docname": self.invoice.name,
|
||||
@ -1135,7 +1185,6 @@ class GSPConnector:
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||
headers["username"] = headers["user_name"]
|
||||
del headers["user_name"]
|
||||
try:
|
||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||
if res.get("success"):
|
||||
@ -1186,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)
|
||||
@ -1228,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",
|
||||
@ -1243,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
|
||||
@ -1293,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)
|
||||
@ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname):
|
||||
# TODO: uncomment when eway_bill api from Adequare is enabled
|
||||
# gsp_connector = GSPConnector(doctype, docname)
|
||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -32,7 +32,7 @@ def _execute(filters=None):
|
||||
added_item = []
|
||||
for d in item_list:
|
||||
if (d.parent, d.item_code) not in added_item:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
|
||||
total_tax = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
@ -40,11 +40,9 @@ def _execute(filters=None):
|
||||
|
||||
row += [d.base_net_amount + total_tax]
|
||||
row += [d.base_net_amount]
|
||||
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
row += [item_tax.get("tax_amount", 0)]
|
||||
|
||||
data.append(row)
|
||||
added_item.append((d.parent, d.item_code))
|
||||
if data:
|
||||
@ -64,6 +62,7 @@ def get_columns():
|
||||
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
|
||||
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
|
||||
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
|
||||
{"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90},
|
||||
{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@ -106,16 +105,25 @@ def get_items(filters):
|
||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description,
|
||||
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
|
||||
from
|
||||
`tabSales Invoice`,
|
||||
`tabSales Invoice Item`,
|
||||
`tabGST HSN Code`,
|
||||
`tabSales Taxes and Charges`
|
||||
where
|
||||
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||
and `tabSales Invoice`.docstatus = 1
|
||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||
group by
|
||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
|
||||
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code
|
||||
"""
|
||||
% (conditions, match_conditions),
|
||||
filters,
|
||||
@ -213,15 +221,16 @@ def get_merged_data(columns, data):
|
||||
result = []
|
||||
|
||||
for row in data:
|
||||
merged_hsn_dict.setdefault(row[0], {})
|
||||
key = row[0] + "-" + str(row[4])
|
||||
merged_hsn_dict.setdefault(key, {})
|
||||
for i, d in enumerate(columns):
|
||||
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
||||
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
|
||||
merged_hsn_dict[key][d["fieldname"]] = row[i]
|
||||
else:
|
||||
if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""):
|
||||
merged_hsn_dict[row[0]][d["fieldname"]] += row[i]
|
||||
if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""):
|
||||
merged_hsn_dict[key][d["fieldname"]] += row[i]
|
||||
else:
|
||||
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
|
||||
merged_hsn_dict[key][d["fieldname"]] = row[i]
|
||||
|
||||
for key, value in merged_hsn_dict.items():
|
||||
result.append(value)
|
||||
@ -240,7 +249,7 @@ def get_json(filters, report_name, data):
|
||||
|
||||
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
||||
|
||||
gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp}
|
||||
gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp}
|
||||
|
||||
gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)}
|
||||
|
||||
@ -271,7 +280,7 @@ def get_hsn_wise_json_data(filters, report_data):
|
||||
"desc": hsn.get("description"),
|
||||
"uqc": hsn.get("stock_uom").upper(),
|
||||
"qty": hsn.get("stock_qty"),
|
||||
"val": flt(hsn.get("total_amount"), 2),
|
||||
"rt": flt(hsn.get("tax_rate"), 2),
|
||||
"txval": flt(hsn.get("taxable_amount", 2)),
|
||||
"iamt": 0.0,
|
||||
"camt": 0.0,
|
||||
|
@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class {
|
||||
frappe.dom.freeze();
|
||||
this.frm = this.get_new_frm(this.frm);
|
||||
this.frm.doc.items = [];
|
||||
const res = await frappe.call({
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||
args: {
|
||||
'source_name': doc.name,
|
||||
'target_doc': this.frm.doc
|
||||
},
|
||||
callback: (r) => {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
|
||||
this.set_pos_profile_data().then(() => {
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
}
|
||||
});
|
||||
frappe.model.sync(res.message);
|
||||
await this.set_pos_profile_data();
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
|
||||
set_pos_profile_data() {
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
EmptyStockReconciliationItemsError,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
@ -180,9 +183,12 @@ def make_items():
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
create_item(item_code)
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
|
||||
)
|
||||
try:
|
||||
create_stock_reconciliation(
|
||||
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
|
||||
)
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
|
||||
if frappe.db.exists("Item", "Test FG A RW 1"):
|
||||
doc = frappe.get_doc("Item", "Test FG A RW 1")
|
||||
|
@ -652,6 +652,104 @@ class TestStockEntry(FrappeTestCase):
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
|
||||
|
||||
def test_serial_batch_item_stock_entry(self):
|
||||
"""
|
||||
Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
|
||||
2) Cancel same Stock Entry
|
||||
Expected Result: 1) Batch is created with Reference in Serial No
|
||||
2) Batch is deleted and Serial No is Inactive
|
||||
"""
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||
)
|
||||
batch_no = se.items[0].batch_no
|
||||
serial_no = get_serial_nos(se.items[0].serial_no)[0]
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
|
||||
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||
self.assertEqual(batch_in_serial_no, batch_no)
|
||||
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
se.cancel()
|
||||
|
||||
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||
self.assertEqual(batch_in_serial_no, None)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
|
||||
def test_serial_batch_item_qty_deduction(self):
|
||||
"""
|
||||
Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
|
||||
Expected: 1) Cancelling first Stock Entry (origin transaction of created batch)
|
||||
should throw a LinkExistsError
|
||||
2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
|
||||
and in that transaction only, Inactive.
|
||||
"""
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||
)
|
||||
batch_no = se1.items[0].batch_no
|
||||
serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
|
||||
|
||||
# Check Source (Origin) Document of Batch
|
||||
self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
|
||||
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
self.assertEqual(batch_qty, 2)
|
||||
|
||||
se2.cancel()
|
||||
|
||||
# Check decrease in Batch Qty
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
# Check if Serial No from Stock Entry 1 is intact
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
|
||||
|
||||
# Check if Serial No from Stock Entry 2 is Unlinked and Inactive
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
||||
frappe.get_doc("User", "test2@example.com").add_roles(
|
||||
|
@ -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)
|
||||
|
@ -62,6 +62,7 @@ class StockReconciliation(StockController):
|
||||
self.make_sle_on_cancel()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def remove_items_with_no_change(self):
|
||||
"""Remove items if qty or rate is not changed"""
|
||||
@ -456,7 +457,7 @@ class StockReconciliation(StockController):
|
||||
|
||||
key = (d.item_code, d.warehouse)
|
||||
if key not in merge_similar_entries:
|
||||
d.total_amount = d.actual_qty * d.valuation_rate
|
||||
d.total_amount = flt(d.actual_qty) * d.valuation_rate
|
||||
merge_similar_entries[key] = d
|
||||
elif d.serial_no:
|
||||
data = merge_similar_entries[key]
|
||||
|
@ -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")
|
||||
@ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
|
||||
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1
|
||||
)
|
||||
sr.save()
|
||||
sr.submit()
|
||||
@ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||
stock_doc.cancel()
|
||||
|
||||
def test_stock_reco_for_serial_and_batch_item(self):
|
||||
item = create_item("_TestBatchSerialItemReco")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "TBS-BATCH-.##"
|
||||
item.serial_no_series = "TBS-.####"
|
||||
item.save()
|
||||
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100)
|
||||
|
||||
batch_no = sr.items[0].batch_no
|
||||
|
||||
serial_nos = get_serial_nos(sr.items[0].serial_no)
|
||||
self.assertEqual(len(serial_nos), 1)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
|
||||
|
||||
sr.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
|
||||
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
|
||||
"""
|
||||
Behaviour: 1) Create Stock Reconciliation, which will be the origin document
|
||||
of a new batch having a serial no
|
||||
2) Create a Stock Entry that adds a serial no to the same batch following this
|
||||
Stock Reconciliation
|
||||
3) Cancel Stock Entry
|
||||
Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases
|
||||
"""
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
item = create_item("_TestBatchSerialItemDependentReco")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "TBSD-BATCH-.##"
|
||||
item.serial_no_series = "TBSD-.####"
|
||||
item.save()
|
||||
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
stock_reco = create_stock_reconciliation(
|
||||
item_code=item.item_code, warehouse=warehouse, qty=1, rate=100
|
||||
)
|
||||
batch_no = stock_reco.items[0].batch_no
|
||||
reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no
|
||||
)
|
||||
serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
|
||||
|
||||
# Check Batch qty after 2 transactions
|
||||
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||
self.assertEqual(batch_qty, 2)
|
||||
|
||||
# Cancel latest stock document
|
||||
stock_entry.cancel()
|
||||
|
||||
# Check Batch qty after cancellation
|
||||
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
# Check if Serial No from Stock Reconcilation is intact
|
||||
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active")
|
||||
|
||||
# Check if Serial No from Stock Entry is Unlinked and Inactive
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
|
||||
|
||||
stock_reco.cancel()
|
||||
|
||||
def test_customer_provided_items(self):
|
||||
item_code = "Stock-Reco-customer-Item-100"
|
||||
create_item(
|
||||
@ -306,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)
|
||||
@ -315,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)
|
||||
)
|
||||
@ -324,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):
|
||||
@ -485,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"
|
||||
@ -684,11 +746,13 @@ def create_stock_reconciliation(**args):
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
if not args.do_not_submit:
|
||||
sr.submit()
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
if not args.do_not_save:
|
||||
sr.insert()
|
||||
try:
|
||||
if not args.do_not_submit:
|
||||
sr.submit()
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
return sr
|
||||
|
||||
|
||||
|
@ -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"),
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user