Merge branch 'develop' into feat-picklist-scan

This commit is contained in:
Ankush Menat 2022-05-11 18:38:53 +05:30
commit a4f0be6c5e
56 changed files with 1694 additions and 873 deletions

View File

@ -27,7 +27,6 @@
"bank_account_no",
"address_and_contact",
"address_html",
"website",
"column_break_13",
"contact_html",
"integration_details_section",
@ -156,11 +155,6 @@
"fieldtype": "HTML",
"label": "Address HTML"
},
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
@ -208,7 +202,7 @@
}
],
"links": [],
"modified": "2020-10-23 16:48:06.303658",
"modified": "2022-05-04 15:49:42.620630",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@ -243,5 +237,6 @@
"search_fields": "bank,account",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -118,6 +118,7 @@ class BankClearance(Document):
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))

View File

@ -467,6 +467,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)

View File

@ -12,7 +12,10 @@ def get_data():
"Sales Invoice": "return_against",
"Auto Repeat": "reference_document",
},
"internal_links": {"Sales Order": ["items", "sales_order"]},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [
{
"label": _("Payment"),

View File

@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset
einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country
def test_einvoice_json(self):

View File

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

View File

@ -203,7 +203,7 @@ def get_loan_entries(filters):
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = (
query = (
frappe.qb.from_(loan_doc)
.select(
ConstantColumn(doctype).as_("payment_document"),
@ -217,9 +217,12 @@ def get_loan_entries(filters):
.where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
.run(as_dict=1)
)
if doctype == "Loan Repayment":
query.where(loan_doc.repay_from_salary == 0)
entries = query.run(as_dict=1)
loan_docs.extend(entries)
return loan_docs

View File

@ -435,7 +435,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle_map[group_by_value].entries.append(gle)
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
keylist = [
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

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

View File

@ -114,11 +114,11 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
onload: (report) => {
// Create a button for setting the default supplier
report.page.add_inner_button(__("Select Default Supplier"), () => {
let reporter = frappe.query_reports["Quoted Item Comparison"];
let reporter = frappe.query_reports["Supplier Quotation Comparison"];
//Always make a new one so that the latest values get updated
reporter.make_default_supplier_dialog(report);
}, 'Tools');
}, __("Tools"));
},
make_default_supplier_dialog: (report) => {
@ -126,7 +126,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
if(!report.data) return;
let filters = report.get_values();
let item_code = filters.item;
let item_code = filters.item_code;
// Get a list of the suppliers (with a blank as well) for the user to select
let suppliers = $.map(report.data, (row, idx)=>{ return row.supplier_name })
@ -152,7 +152,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
]
});
dialog.set_primary_action("Set Default Supplier", () => {
dialog.set_primary_action(__("Set Default Supplier"), () => {
let values = dialog.get_values();
if(values) {
// Set the default_supplier field of the appropriate Item to the selected supplier

View File

@ -2451,11 +2451,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")):
frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
)
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
child_item.received_qty
):
frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items)

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname
from frappe.utils import cint, flt, get_fullname
from erpnext.crm.utils import add_link_in_communication, copy_comments
from erpnext.setup.utils import get_exchange_rate
@ -215,20 +215,20 @@ class Opportunity(TransactionBase):
if self.party_name and self.opportunity_from == "Customer":
if self.contact_person:
opts.description = "Contact " + cstr(self.contact_person)
opts.description = f"Contact {self.contact_person}"
else:
opts.description = "Contact customer " + cstr(self.party_name)
opts.description = f"Contact customer {self.party_name}"
elif self.party_name and self.opportunity_from == "Lead":
if self.contact_display:
opts.description = "Contact " + cstr(self.contact_display)
opts.description = f"Contact {self.contact_display}"
else:
opts.description = "Contact lead " + cstr(self.party_name)
opts.description = f"Contact lead {self.party_name}"
opts.subject = opts.description
opts.description += ". By : " + cstr(self.contact_by)
opts.description += f". By : {self.contact_by}"
if self.to_discuss:
opts.description += " To Discuss : " + cstr(self.to_discuss)
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
super(Opportunity, self).add_calendar_event(opts, force)

View File

@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import now_datetime, random_string, today
from frappe.utils import add_days, now_datetime, random_string, today
from erpnext.crm.doctype.lead.lead import make_customer
from erpnext.crm.doctype.lead.test_lead import make_lead
@ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase):
self.assertEqual(quotation_comment_count, 4)
self.assertEqual(quotation_communication_count, 4)
def test_render_template_for_to_discuss(self):
doc = make_opportunity(with_items=0, opportunity_from="Lead")
doc.contact_by = "test@example.com"
doc.contact_date = add_days(today(), days=2)
doc.to_discuss = "{{ doc.name }} test data"
doc.save()
event = frappe.get_all(
"Event Participants",
fields=["parent"],
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
)
event_description = frappe.db.get_value("Event", event[0].parent, "description")
self.assertTrue(doc.name in event_description)
def make_opportunity_from_lead():
new_lead_email_id = "new{}@example.com".format(random_string(5))

View File

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

View File

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

View File

@ -8,7 +8,9 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"default_salary_structure"
"default_salary_structure",
"currency",
"default_base_pay"
],
"fields": [
{
@ -16,14 +18,31 @@
"fieldtype": "Link",
"label": "Default Salary Structure",
"options": "Salary Structure"
},
{
"depends_on": "default_salary_structure",
"fieldname": "default_base_pay",
"fieldtype": "Currency",
"label": "Default Base Pay",
"options": "currency"
},
{
"fetch_from": "default_salary_structure.currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-26 13:12:07.815330",
"modified": "2022-05-06 15:42:10.395508",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grade",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -65,5 +84,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

@ -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:

View File

@ -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(
{

View File

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

View File

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

View File

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

View File

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

View File

@ -369,4 +369,5 @@ erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note

View File

@ -0,0 +1,29 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
def execute():
dn = frappe.qb.DocType("Delivery Note")
dn_item = frappe.qb.DocType("Delivery Note Item")
dn_list = (
frappe.qb.from_(dn)
.inner_join(dn_item)
.on(dn.name == dn_item.parent)
.select(dn.name)
.where(dn.docstatus == 1)
.where(dn.is_return == 1)
.where(dn.per_billed < 100)
.where(dn_item.returned_qty > 0)
.run(as_dict=True)
)
frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
dn_item.returned_qty, 0
).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
for d in dn_list:
dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
dn_doc.run_method("update_billing_status")

View File

@ -164,6 +164,15 @@ frappe.ui.form.on('Salary Structure', {
primary_action_label: __('Assign')
});
d.fields_dict.grade.df.onchange = function() {
const grade = d.fields_dict.grade.value;
if (grade) {
frappe.db.get_value('Employee Grade', grade, 'default_base_pay')
.then(({ message }) => {
d.set_value('base', message.default_base_pay);
});
}
};
d.show();
},

View File

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_years, date_diff, get_first_day, nowdate
from frappe.utils.make_random import get_random
@ -23,7 +24,7 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
test_dependencies = ["Fiscal Year"]
class TestSalaryStructure(unittest.TestCase):
class TestSalaryStructure(FrappeTestCase):
def setUp(self):
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]:
frappe.db.sql("delete from `tab%s`" % dt)
@ -132,6 +133,23 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(salary_structure_assignment.base, 5000)
self.assertEqual(salary_structure_assignment.variable, 200)
def test_employee_grade_defaults(self):
salary_structure = make_salary_structure(
"Salary Structure - Lead", "Monthly", currency="INR", company="_Test Company"
)
create_employee_grade("Lead", salary_structure.name)
employee = make_employee("test_employee_grade@salary.com", company="_Test Company", grade="Lead")
# structure assignment should have the default salary structure and base pay
salary_structure.assign_salary_structure(employee=employee, from_date=nowdate())
structure, base = frappe.db.get_value(
"Salary Structure Assignment",
{"employee": employee, "salary_structure": salary_structure.name, "from_date": nowdate()},
["salary_structure", "base"],
)
self.assertEqual(structure, salary_structure.name)
self.assertEqual(base, 50000)
def test_multi_currency_salary_structure(self):
make_employee("test_muti_currency_employee@salary.com")
sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency="USD")
@ -251,3 +269,15 @@ def get_payable_account(company=None):
if not company:
company = erpnext.get_default_company()
return frappe.db.get_value("Company", company, "default_payroll_payable_account")
def create_employee_grade(grade, default_structure=None):
if not frappe.db.exists("Employee Grade", grade):
frappe.get_doc(
{
"doctype": "Employee Grade",
"__newname": grade,
"default_salary_structure": default_structure,
"default_base_pay": 50000,
}
).insert()

View File

@ -10,6 +10,7 @@
"employee",
"employee_name",
"department",
"grade",
"company",
"payroll_payable_account",
"column_break_6",
@ -67,6 +68,8 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "grade.default_salary_structure",
"fetch_if_empty": 1,
"fieldname": "salary_structure",
"fieldtype": "Link",
"in_list_view": 1,
@ -96,6 +99,8 @@
"label": "Base & Variable"
},
{
"fetch_from": "grade.default_base_pay",
"fetch_if_empty": 1,
"fieldname": "base",
"fieldtype": "Currency",
"label": "Base",
@ -158,11 +163,19 @@
"fieldtype": "Table",
"label": "Cost Centers",
"options": "Employee Cost Center"
},
{
"fetch_from": "employee.grade",
"fieldname": "grade",
"fieldtype": "Link",
"label": "Grade",
"options": "Employee Grade",
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-01-19 12:43:54.439073",
"modified": "2022-05-06 12:18:36.972336",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",

View File

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

View File

@ -167,7 +167,12 @@ def get_doc_details(invoice):
title=_("Not Allowed"),
)
invoice_type = "CRN" if invoice.is_return else "INV"
if invoice.is_return:
invoice_type = "CRN"
elif invoice.is_debit_note:
invoice_type = "DBN"
else:
invoice_type = "INV"
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
@ -443,7 +448,7 @@ def get_eway_bill_details(invoice):
dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None,
distance=invoice.distance or 0,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_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()

View File

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

View File

@ -65,7 +65,11 @@ frappe.ui.form.on("Sales Order", {
frm.set_value('transaction_date', frappe.datetime.get_today())
}
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
return {
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
]
};
});
frm.set_query('project', function(doc, cdt, cdn) {
@ -77,7 +81,19 @@ frappe.ui.form.on("Sales Order", {
}
});
erpnext.queries.setup_warehouse_query(frm);
frm.set_query('warehouse', 'items', function(doc, cdt, cdn) {
let row = locals[cdt][cdn];
let query = {
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
]
};
if (row.item_code) {
query.query = "erpnext.controllers.queries.warehouse_query";
query.filters.push(["Bin", "item_code", "=", row.item_code]);
}
return query;
});
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
},

View File

@ -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() {

View File

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

View File

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

View File

@ -415,3 +415,8 @@ class Analytics(object):
else:
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
if self.filters["value_quantity"] == "Value":
self.chart["fieldtype"] = "Currency"
else:
self.chart["fieldtype"] = "Float"

View File

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

View File

@ -4,10 +4,13 @@
frappe.ui.form.on("Naming Series", {
onload: function(frm) {
frm.disable_save();
frm.events.get_doc_and_prefix(frm);
},
refresh: function(frm) {
frm.disable_save();
},
get_doc_and_prefix: function(frm) {
frappe.call({
method: "get_transactions",

View File

@ -962,6 +962,44 @@ class TestDeliveryNote(FrappeTestCase):
automatically_fetch_payment_terms(enable=0)
def test_returned_qty_in_return_dn(self):
# SO ---> SI ---> DN
# |
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
# |
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=10)
si = make_sales_invoice(so.name)
si.insert()
si.submit()
dn = make_delivery_note(si.name)
dn.insert()
dn.submit()
self.assertEqual(dn.items[0].returned_qty, 0)
self.assertEqual(dn.per_billed, 100)
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
si1 = make_sales_invoice(dn1.name)
si1.insert()
si1.submit()
dn1.reload()
self.assertEqual(dn1.items[0].returned_qty, 0)
self.assertEqual(dn1.per_billed, 100)
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
si2 = make_sales_invoice(dn2.name)
si2.insert()
si2.submit()
dn2.reload()
self.assertEqual(dn2.items[0].returned_qty, 0)
self.assertEqual(dn2.per_billed, 100)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@ -737,7 +737,9 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty in Stock UOM"
"label": "Returned Qty in Stock UOM",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "incoming_rate",
@ -778,7 +780,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-31 18:36:24.671913",
"modified": "2022-05-02 12:09:39.610075",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

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

View File

@ -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(

View File

@ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase):
backdated.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
def test_timestamp_clash(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reciept = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="01:00:00",
)
consumption = make_stock_entry(
item_code=item,
from_warehouse=warehouse,
qty=50,
posting_date="2021-01-01",
posting_time="02:00:00.1234", # ms are possible when submitted without editing posting time
)
backdated_receipt = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00", # same posting time as consumption but ms part stripped
)
try:
backdated_receipt.cancel()
except Exception as e:
self.fail("Double processing of qty for clashing timestamp.")
def create_repack_entry(**args):
args = frappe._dict(args)

View File

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

View File

@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
def tearDown(self):
frappe.local.future_sle = {}
frappe.flags.pop("dont_execute_stock_reposts", None)
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@ -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

View File

@ -0,0 +1,53 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
const DIFFERNCE_FIELD_NAMES = [
"fifo_qty_diff",
"fifo_value_diff",
];
frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
"filters": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
}
}
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Posting Date",
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "From Posting Date",
}
],
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
value = "<span style='color:red'>" + value + "</span>";
}
return value;
},
};

View File

@ -0,0 +1,27 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2022-05-11 04:09:13.460652",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "abc",
"modified": "2022-05-11 04:09:20.232177",
"modified_by": "Administrator",
"module": "Stock",
"name": "FIFO Queue vs Qty After Transaction Comparison",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "FIFO Queue vs Qty After Transaction Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Administrator"
}
]
}

View File

@ -0,0 +1,212 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
SLE_FIELDS = (
"name",
"item_code",
"warehouse",
"posting_date",
"posting_time",
"creation",
"voucher_type",
"voucher_no",
"actual_qty",
"qty_after_transaction",
"stock_queue",
"batch_no",
"stock_value",
"valuation_rate",
)
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters):
if not any([filters.warehouse, filters.item_code, filters.item_group]):
frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
sles = get_stock_ledger_entries(filters)
return find_first_bad_queue(sles)
def get_stock_ledger_entries(filters):
sle_filters = {"is_cancelled": 0}
if filters.warehouse:
children = get_descendants_of("Warehouse", filters.warehouse)
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
if filters.item_code:
sle_filters["item_code"] = filters.item_code
elif filters.get("item_group"):
item_group = filters.get("item_group")
children = get_descendants_of("Item Group", item_group)
item_group_filter = {"item_group": ("in", children + [item_group])}
sle_filters["item_code"] = (
"in",
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
)
if filters.from_date:
sle_filters["posting_date"] = (">=", filters.from_date)
if filters.to_date:
sle_filters["posting_date"] = ("<=", filters.to_date)
return frappe.get_all(
"Stock Ledger Entry",
fields=SLE_FIELDS,
filters=sle_filters,
order_by="timestamp(posting_date, posting_time), creation",
)
def find_first_bad_queue(sles):
item_warehouse_sles = {}
for sle in sles:
item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
data = []
for _item_wh, sles in item_warehouse_sles.items():
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue or "[]")
sle.fifo_queue_qty = 0.0
sle.fifo_stock_value = 0.0
for qty, rate in queue:
sle.fifo_queue_qty += flt(qty)
sle.fifo_stock_value += flt(qty) * flt(rate)
sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
if sle.batch_no:
sle.use_batchwise_valuation = frappe.db.get_value(
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
)
if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
if idx:
data.append(sles[idx - 1])
data.append(sle)
data.append({})
break
return data
def get_columns():
return [
{
"fieldname": "name",
"fieldtype": "Link",
"label": _("Stock Ledger Entry"),
"options": "Stock Ledger Entry",
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": _("Item Code"),
"options": "Item",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": _("Warehouse"),
"options": "Warehouse",
},
{
"fieldname": "posting_date",
"fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
"fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
"fieldtype": "Data",
"label": _("Creation"),
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": _("Voucher Type"),
"options": "DocType",
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": _("Voucher No"),
"options": "voucher_type",
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
"label": _("Batchwise Valuation"),
},
{
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": _("Qty Change"),
},
{
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"label": _("(A) Qty After Transaction"),
},
{
"fieldname": "stock_queue",
"fieldtype": "Data",
"label": _("FIFO/LIFO Queue"),
},
{
"fieldname": "fifo_queue_qty",
"fieldtype": "Float",
"label": _("(C) Total qty in queue"),
},
{
"fieldname": "fifo_qty_diff",
"fieldtype": "Float",
"label": _("A - C"),
},
{
"fieldname": "stock_value",
"fieldtype": "Float",
"label": _("(D) Balance Stock Value"),
},
{
"fieldname": "fifo_stock_value",
"fieldtype": "Float",
"label": _("(E) Balance Stock Value in Queue"),
},
{
"fieldname": "fifo_value_diff",
"fieldtype": "Float",
"label": _("D - E"),
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
"label": _("(H) Valuation Rate"),
},
]

View File

@ -111,17 +111,17 @@ def get_columns():
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
"fieldtype": "Datetime",
"fieldtype": "Data",
"label": _("Creation"),
},
{

View File

@ -65,6 +65,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
]
OPTIONAL_FILTERS = {

View File

@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
datetime_limit_condition = ""
qty_shift = args.actual_qty
args["time_format"] = "%H:%i:%s"
# find difference/shift in qty caused by stock reconciliation
if args.voucher_type == "Stock Reconciliation":
qty_shift = get_stock_reco_qty_shift(args)
@ -1315,7 +1317,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql(
"""
f"""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty_shift}
where
@ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
)
)
and timestamp(posting_date, time_format(posting_time, %(time_format)s))
> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
{datetime_limit_condition}
""".format(
qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
),
""",
args,
)
@ -1383,6 +1379,7 @@ def get_next_stock_reco(args):
and creation > %(creation)s
)
)
order by timestamp(posting_date, posting_time) asc, creation asc
limit 1
""",
args,

View File

@ -0,0 +1,9 @@
from frappe.tests.utils import FrappeTestCase
from erpnext.utilities.activation import get_level
class TestActivation(FrappeTestCase):
def test_activation(self):
levels = get_level()
self.assertTrue(levels)

View File

@ -360,7 +360,7 @@ Bank Statement,Kontoauszug,
Bank Statement Settings,Kontoauszug Einstellungen,
Bank Statement balance as per General Ledger,Kontoauszug Bilanz nach Hauptbuch,
Bank account cannot be named as {0},Bankname {0} ungültig,
Bank/Cash transactions against party or for internal transfer,Bank / Geldgeschäfte gegen Partei oder für die interne Übertragung,
Bank/Cash transactions against party or for internal transfer,Bank-/Bargeldtransaktionen mit einer Partei oder intern,
Banking,Bankwesen,
Banking and Payments,Bank- und Zahlungsverkehr,
Barcode {0} already used in Item {1},Barcode {0} wird bereits für Artikel {1} verwendet,
@ -1098,7 +1098,7 @@ From Datetime,Von Datum und Uhrzeit,
From Delivery Note,Von Lieferschein,
From Fiscal Year,Ab dem Geschäftsjahr,
From GSTIN,Von GSTIN,
From Party Name,Von Party Name,
From Party Name,Name des Absenders,
From Pin Code,Von Pin-Code,
From Place,Von Ort,
From Range has to be less than To Range,Von-Bereich muss kleiner sein als Bis-Bereich,
@ -1174,7 +1174,7 @@ Gross Profit / Loss,Bruttogewinn / Verlust,
Gross Purchase Amount,Bruttokaufbetrag,
Gross Purchase Amount is mandatory,Bruttokaufbetrag ist erforderlich,
Group by Account,Gruppieren nach Konto,
Group by Party,Gruppieren nach Parteien,
Group by Party,Gruppieren nach Partei,
Group by Voucher,Gruppieren nach Beleg,
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt,
@ -1873,12 +1873,12 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme,
Part-time,Teilzeit,
Partially Depreciated,Teilweise abgeschrieben,
Partially Received,Teilweise erhalten,
Party,Gruppe,
Party Name,Name,
Party Type,Gruppen-Typ,
Party Type and Party is mandatory for {0} account,Party Type und Party ist für das Konto {0} obligatorisch,
Party Type is mandatory,Party-Typ ist Pflicht,
Party is mandatory,Partei ist obligatorisch,
Party,Partei,
Party Name,Name der Partei,
Party Type,Partei-Typ,
Party Type and Party is mandatory for {0} account,Partei-Typ und Partei sind Pflichtfelder für Konto {0},
Party Type is mandatory,Partei-Typ ist ein Pflichtfeld,
Party is mandatory,Partei ist ein Pflichtfeld,
Password,Passwort,
Password policy for Salary Slips is not set,Die Kennwortrichtlinie für Gehaltsabrechnungen ist nicht festgelegt,
Past Due Date,Fälligkeitsdatum,
@ -2039,10 +2039,10 @@ Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
"Please select Item where ""Is Stock Item"" is ""No"" and ""Is Sales Item"" is ""Yes"" and there is no other Product Bundle","Bitte einen Artikel auswählen, bei dem ""Ist Lagerartikel"" mit ""Nein"" und ""Ist Verkaufsartikel"" mit ""Ja"" bezeichnet ist, und es kein anderes Produkt-Bundle gibt",
Please select Maintenance Status as Completed or remove Completion Date,Bitte wählen Sie Wartungsstatus als erledigt oder entfernen Sie das Abschlussdatum,
Please select Party Type first,Bitte zuerst Gruppentyp auswählen,
Please select Party Type first,Bitte zuerst Partei-Typ auswählen,
Please select Patient,Bitte einen Patienten auswählen,
Please select Patient to get Lab Tests,"Bitte wählen Sie Patient, um Labortests zu erhalten",
Please select Posting Date before selecting Party,Bitte wählen Sie Buchungsdatum vor dem Party-Auswahl,
Please select Posting Date before selecting Party,Bitte erst Buchungsdatum und dann die Partei auswählen,
Please select Posting Date first,Bitte zuerst ein Buchungsdatum auswählen,
Please select Price List,Bitte eine Preisliste auswählen,
Please select Program,Bitte wählen Sie Programm,
@ -2184,7 +2184,7 @@ Process Day Book Data,Tagesbuchdaten verarbeiten,
Process Master Data,Stammdaten bearbeiten,
Processing Chart of Accounts and Parties,Verarbeiten des Kontenplans und der Parteien,
Processing Items and UOMs,Verarbeiten von Artikeln und Mengeneinheiten,
Processing Party Addresses,Bearbeiteradressen,
Processing Party Addresses,Verarbeitung der Adressen der Parteien,
Processing Vouchers,Bearbeitung von Gutscheinen,
Procurement,Beschaffung,
Produced Qty,Produzierte Menge,
@ -2488,8 +2488,8 @@ Row {0}: From Time and To Time of {1} is overlapping with {2},Zeile {0}: Zeitüb
Row {0}: From time must be less than to time,Zeile {0}: Von Zeit zu Zeit muss kleiner sein,
Row {0}: Hours value must be greater than zero.,Row {0}: Stunden-Wert muss größer als Null sein.,
Row {0}: Invalid reference {1},Zeile {0}: Ungültige Referenz {1},
Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Zeile {0}: Gruppe / Konto stimmt nicht mit {1} / {2} in {3} {4} überein,
Row {0}: Party Type and Party is required for Receivable / Payable account {1},Zeile {0}: Gruppen-Typ und Gruppe sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Zeile {0}: Partei / Konto stimmt nicht mit {1} / {2} in {3} {4} überein,
Row {0}: Party Type and Party is required for Receivable / Payable account {1},Zeile {0}: Partei-Typ und Partei sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
Row {0}: Payment against Sales/Purchase Order should always be marked as advance,"Zeile {0}: ""Zahlung zu Auftrag bzw. Bestellung"" sollte immer als ""Vorkasse"" eingestellt werden",
Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,"Zeile {0}: Wenn es sich um eine Vorkasse-Buchung handelt, bitte ""Ist Vorkasse"" zu Konto {1} anklicken, .",
Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Zeile {0}: Bitte setzen Sie den Steuerbefreiungsgrund in den Umsatzsteuern und -gebühren,
@ -3047,7 +3047,7 @@ To Deliver,Auszuliefern,
To Deliver and Bill,Auszuliefern und Abzurechnen,
To Fiscal Year,Bis zum Geschäftsjahr,
To GSTIN,Zu GSTIN,
To Party Name,Zum Party-Namen,
To Party Name,Name des Empfängers,
To Pin Code,PIN-Code,
To Place,Hinstellen,
To Receive,Zu empfangen,
@ -3058,7 +3058,7 @@ To create a Payment Request reference document is required,Zur Erstellung eines
To date can not be equal or less than from date,Bis heute kann nicht gleich oder weniger als von Datum sein,
To date can not be less than from date,Bis heute kann nicht weniger als von Datum sein,
To date can not greater than employee's relieving date,Bis heute kann nicht mehr als Entlastungsdatum des Mitarbeiters sein,
"To filter based on Party, select Party Type first","Um auf der Grundlage von Gruppen zu filtern, bitte zuerst den Gruppentyp wählen",
"To filter based on Party, select Party Type first","Bitte Partei-Typ wählen um nach Partei zu filtern",
"To get the best out of ERPNext, we recommend that you take some time and watch these help videos.","Um ERPNext bestmöglich zu nutzen, empfehlen wir Ihnen, sich die Zeit zu nehmen diese Hilfevideos anzusehen.",
"To include tax in row {0} in Item rate, taxes in rows {1} must also be included","Um Steuern im Artikelpreis in Zeile {0} einzubeziehen, müssen Steuern in den Zeilen {1} ebenfalls einbezogen sein",
To make Customer based incentive schemes.,Um Kunden basierte Anreizsysteme zu machen.,
@ -4550,7 +4550,7 @@ Company Account,Firmenkonto,
Account Subtype,Kontosubtyp,
Is Default Account,Ist Standardkonto,
Is Company Account,Ist Unternehmenskonto,
Party Details,Party Details,
Party Details,Details der Partei,
Account Details,Kontendaten,
IBAN,IBAN,
Bank Account No,Bankkonto Nr,
@ -4786,9 +4786,9 @@ Payment Order,Zahlungsauftrag,
Subscription Section,Abonnementbereich,
Journal Entry Account,Journalbuchungskonto,
Account Balance,Kontostand,
Party Balance,Gruppen-Saldo,
Party Balance,Saldo der Partei,
Accounting Dimensions,Abrechnungsdimensionen,
If Income or Expense,Wenn Ertrag oder Aufwand,
If Income or Expense,Wenn Ertrag oder Aufwand,
Exchange Rate,Wechselkurs,
Debit in Company Currency,Soll in Unternehmenswährung,
Credit in Company Currency,(Gut)Haben in Unternehmenswährung,
@ -4829,11 +4829,11 @@ Name of the Monthly Distribution,Bezeichnung der monatsweisen Verteilung,
Monthly Distribution Percentages,Prozentuale Aufteilungen der monatsweisen Verteilung,
Monthly Distribution Percentage,Prozentuale Aufteilung der monatsweisen Verteilung,
Percentage Allocation,Prozentuale Aufteilung,
Create Missing Party,Erstelle fehlende Partei,
Create Missing Party,Fehlende Partei erstellen,
Create missing customer or supplier.,Erstelle einen fehlenden Kunden oder Lieferanten.,
Opening Invoice Creation Tool Item,Eröffnen des Rechnungserstellungswerkzeugs,
Temporary Opening Account,Temporäres Eröffnungskonto,
Party Account,Gruppenkonto,
Party Account,Konto der Partei,
Type of Payment,Zahlungsart,
ACC-PAY-.YYYY.-,ACC-PAY-.JJJJ.-,
Receive,Empfangen,
@ -4842,7 +4842,7 @@ Payment Order Status,Zahlungsauftragsstatus,
Payment Ordered,Zahlung bestellt,
Payment From / To,Zahlung von / an,
Company Bank Account,Firmenkonto,
Party Bank Account,Party-Bankkonto,
Party Bank Account,Bankkonto der Partei,
Account Paid From,Ausgangskonto,
Account Paid To,Eingangskonto,
Paid Amount (Company Currency),Gezahlter Betrag (Unternehmenswährung),
@ -4946,7 +4946,7 @@ Is Cumulative,Ist kumulativ,
Coupon Code Based,Gutscheincode basiert,
Discount on Other Item,Rabatt auf andere Artikel,
Apply Rule On Other,Regel auf andere anwenden,
Party Information,Party Informationen,
Party Information,Informationen zur Partei,
Quantity and Amount,Menge und Menge,
Min Qty,Mindestmenge,
Max Qty,Maximalmenge,
@ -5048,7 +5048,7 @@ Group same items,Gruppe gleichen Artikel,
Print Language,Drucksprache,
"Once set, this invoice will be on hold till the set date","Einmal eingestellt, wird diese Rechnung bis zum festgelegten Datum gehalten",
Credit To,Gutschreiben auf,
Party Account Currency,Gruppenkonten-Währung,
Party Account Currency,Währung des Kontos der Partei,
Against Expense Account,Zu Aufwandskonto,
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
Is Internal Supplier,Ist interner Lieferant,
@ -5650,7 +5650,7 @@ From Time ,Von-Zeit,
Campaign Email Schedule,Kampagnen-E-Mail-Zeitplan,
Send After (days),Senden nach (Tage),
Signed,Unterzeichnet,
Party User,Party Benutzer,
Party User,Benutzer der Partei,
Unsigned,Nicht unterzeichnet,
Fulfilment Status,Erfüllungsstatus,
N/A,nicht verfügbar,
@ -8598,7 +8598,7 @@ Territory-wise Sales,Gebietsbezogene Verkäufe,
Total Stock Summary,Gesamt Stock Zusammenfassung,
Trial Balance,Probebilanz,
Trial Balance (Simple),Probebilanz (einfach),
Trial Balance for Party,Summen- und Saldenliste für Gruppe,
Trial Balance for Party,Summen- und Saldenliste für Partei,
Unpaid Expense Claim,Ungezahlte Spesenabrechnung,
Warehouse wise Item Balance Age and Value,Lagerweise Item Balance Alter und Wert,
Work Order Stock Report,Arbeitsauftragsbericht,
@ -9533,7 +9533,7 @@ Account {0} exists in parent company {1}.,Konto {0} existiert in der Muttergesel
"To overrule this, enable '{0}' in company {1}","Um dies zu überschreiben, aktivieren Sie &#39;{0}&#39; in Firma {1}",
Invalid condition expression,Ungültiger Bedingungsausdruck,
Please Select a Company First,Bitte wählen Sie zuerst eine Firma aus,
Please Select Both Company and Party Type First,Bitte wählen Sie zuerst Firmen- und Partytyp aus,
Please Select Both Company and Party Type First,Bitte zuerst Unternehmen und Partei-Typ auswählen,
Provide the invoice portion in percent,Geben Sie den Rechnungsanteil in Prozent an,
Give number of days according to prior selection,Geben Sie die Anzahl der Tage gemäß vorheriger Auswahl an,
Email Details,E-Mail-Details,

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

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,6 @@ def get_level():
"Customer": 5,
"Delivery Note": 5,
"Employee": 3,
"Instructor": 5,
"Issue": 5,
"Item": 5,
"Journal Entry": 3,