Merge branch 'develop' into feat-picklist-scan
This commit is contained in:
commit
a4f0be6c5e
@ -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
|
||||
}
|
@ -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]))
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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"),
|
||||
|
@ -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"))))
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -2451,11 +2451,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
||||
)
|
||||
|
||||
def validate_quantity(child_item, d):
|
||||
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
|
||||
def validate_quantity(child_item, new_data):
|
||||
if not flt(new_data.get("qty")):
|
||||
frappe.throw(
|
||||
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
||||
|
||||
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
|
||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
|
||||
child_item.received_qty
|
||||
):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, cstr, flt, get_fullname
|
||||
from frappe.utils import cint, flt, get_fullname
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@ -215,20 +215,20 @@ class Opportunity(TransactionBase):
|
||||
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
if self.contact_person:
|
||||
opts.description = "Contact " + cstr(self.contact_person)
|
||||
opts.description = f"Contact {self.contact_person}"
|
||||
else:
|
||||
opts.description = "Contact customer " + cstr(self.party_name)
|
||||
opts.description = f"Contact customer {self.party_name}"
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
if self.contact_display:
|
||||
opts.description = "Contact " + cstr(self.contact_display)
|
||||
opts.description = f"Contact {self.contact_display}"
|
||||
else:
|
||||
opts.description = "Contact lead " + cstr(self.party_name)
|
||||
opts.description = f"Contact lead {self.party_name}"
|
||||
|
||||
opts.subject = opts.description
|
||||
opts.description += ". By : " + cstr(self.contact_by)
|
||||
opts.description += f". By : {self.contact_by}"
|
||||
|
||||
if self.to_discuss:
|
||||
opts.description += " To Discuss : " + cstr(self.to_discuss)
|
||||
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||
|
||||
super(Opportunity, self).add_calendar_event(opts, force)
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, random_string, today
|
||||
from frappe.utils import add_days, now_datetime, random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_customer
|
||||
from erpnext.crm.doctype.lead.test_lead import make_lead
|
||||
@ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase):
|
||||
self.assertEqual(quotation_comment_count, 4)
|
||||
self.assertEqual(quotation_communication_count, 4)
|
||||
|
||||
def test_render_template_for_to_discuss(self):
|
||||
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||
doc.contact_by = "test@example.com"
|
||||
doc.contact_date = add_days(today(), days=2)
|
||||
doc.to_discuss = "{{ doc.name }} test data"
|
||||
doc.save()
|
||||
|
||||
event = frappe.get_all(
|
||||
"Event Participants",
|
||||
fields=["parent"],
|
||||
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||
)
|
||||
|
||||
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||
self.assertTrue(doc.name in event_description)
|
||||
|
||||
|
||||
def make_opportunity_from_lead():
|
||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
@ -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):
|
||||
|
@ -369,4 +369,5 @@ erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
|
||||
erpnext.patches.v14_0.discount_accounting_separation
|
||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
dn = frappe.qb.DocType("Delivery Note")
|
||||
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
dn_list = (
|
||||
frappe.qb.from_(dn)
|
||||
.inner_join(dn_item)
|
||||
.on(dn.name == dn_item.parent)
|
||||
.select(dn.name)
|
||||
.where(dn.docstatus == 1)
|
||||
.where(dn.is_return == 1)
|
||||
.where(dn.per_billed < 100)
|
||||
.where(dn_item.returned_qty > 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
|
||||
dn_item.returned_qty, 0
|
||||
).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
|
||||
|
||||
for d in dn_list:
|
||||
dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
|
||||
dn_doc.run_method("update_billing_status")
|
@ -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();
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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'];
|
||||
},
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -962,6 +962,44 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_returned_qty_in_return_dn(self):
|
||||
# SO ---> SI ---> DN
|
||||
# |
|
||||
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||
# |
|
||||
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
so = make_sales_order(qty=10)
|
||||
si = make_sales_invoice(so.name)
|
||||
si.insert()
|
||||
si.submit()
|
||||
dn = make_delivery_note(si.name)
|
||||
dn.insert()
|
||||
dn.submit()
|
||||
self.assertEqual(dn.items[0].returned_qty, 0)
|
||||
self.assertEqual(dn.per_billed, 100)
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
|
||||
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
|
||||
si1 = make_sales_invoice(dn1.name)
|
||||
si1.insert()
|
||||
si1.submit()
|
||||
dn1.reload()
|
||||
self.assertEqual(dn1.items[0].returned_qty, 0)
|
||||
self.assertEqual(dn1.per_billed, 100)
|
||||
|
||||
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
|
||||
si2 = make_sales_invoice(dn2.name)
|
||||
si2.insert()
|
||||
si2.submit()
|
||||
dn2.reload()
|
||||
self.assertEqual(dn2.items[0].returned_qty, 0)
|
||||
self.assertEqual(dn2.per_billed, 100)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -737,7 +737,9 @@
|
||||
"depends_on": "returned_qty",
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty in Stock UOM"
|
||||
"label": "Returned Qty in Stock UOM",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "incoming_rate",
|
||||
@ -778,7 +780,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-31 18:36:24.671913",
|
||||
"modified": "2022-05-02 12:09:39.610075",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
const DIFFERNCE_FIELD_NAMES = [
|
||||
"fifo_qty_diff",
|
||||
"fifo_value_diff",
|
||||
];
|
||||
|
||||
frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {is_stock_item: 1, has_serial_no: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Posting Date",
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Posting Date",
|
||||
}
|
||||
],
|
||||
formatter (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2022-05-11 04:09:13.460652",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "abc",
|
||||
"modified": "2022-05-11 04:09:20.232177",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Administrator"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
SLE_FIELDS = (
|
||||
"name",
|
||||
"item_code",
|
||||
"warehouse",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"creation",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"actual_qty",
|
||||
"qty_after_transaction",
|
||||
"stock_queue",
|
||||
"batch_no",
|
||||
"stock_value",
|
||||
"valuation_rate",
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
if not any([filters.warehouse, filters.item_code, filters.item_group]):
|
||||
frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
|
||||
sles = get_stock_ledger_entries(filters)
|
||||
return find_first_bad_queue(sles)
|
||||
|
||||
|
||||
def get_stock_ledger_entries(filters):
|
||||
|
||||
sle_filters = {"is_cancelled": 0}
|
||||
|
||||
if filters.warehouse:
|
||||
children = get_descendants_of("Warehouse", filters.warehouse)
|
||||
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
|
||||
|
||||
if filters.item_code:
|
||||
sle_filters["item_code"] = filters.item_code
|
||||
elif filters.get("item_group"):
|
||||
item_group = filters.get("item_group")
|
||||
children = get_descendants_of("Item Group", item_group)
|
||||
item_group_filter = {"item_group": ("in", children + [item_group])}
|
||||
sle_filters["item_code"] = (
|
||||
"in",
|
||||
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
|
||||
)
|
||||
|
||||
if filters.from_date:
|
||||
sle_filters["posting_date"] = (">=", filters.from_date)
|
||||
if filters.to_date:
|
||||
sle_filters["posting_date"] = ("<=", filters.to_date)
|
||||
|
||||
return frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=SLE_FIELDS,
|
||||
filters=sle_filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation",
|
||||
)
|
||||
|
||||
|
||||
def find_first_bad_queue(sles):
|
||||
item_warehouse_sles = {}
|
||||
for sle in sles:
|
||||
item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
|
||||
|
||||
data = []
|
||||
|
||||
for _item_wh, sles in item_warehouse_sles.items():
|
||||
for idx, sle in enumerate(sles):
|
||||
queue = json.loads(sle.stock_queue or "[]")
|
||||
|
||||
sle.fifo_queue_qty = 0.0
|
||||
sle.fifo_stock_value = 0.0
|
||||
for qty, rate in queue:
|
||||
sle.fifo_queue_qty += flt(qty)
|
||||
sle.fifo_stock_value += flt(qty) * flt(rate)
|
||||
|
||||
sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
|
||||
sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
|
||||
|
||||
if sle.batch_no:
|
||||
sle.use_batchwise_valuation = frappe.db.get_value(
|
||||
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
|
||||
)
|
||||
|
||||
if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
|
||||
if idx:
|
||||
data.append(sles[idx - 1])
|
||||
data.append(sle)
|
||||
data.append({})
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Stock Ledger Entry"),
|
||||
"options": "Stock Ledger Entry",
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Item Code"),
|
||||
"options": "Item",
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Date"),
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Time"),
|
||||
},
|
||||
{
|
||||
"fieldname": "creation",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Creation"),
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Voucher Type"),
|
||||
"options": "DocType",
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": _("Voucher No"),
|
||||
"options": "voucher_type",
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Batch"),
|
||||
"options": "Batch",
|
||||
},
|
||||
{
|
||||
"fieldname": "use_batchwise_valuation",
|
||||
"fieldtype": "Check",
|
||||
"label": _("Batchwise Valuation"),
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("Qty Change"),
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_after_transaction",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(A) Qty After Transaction"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_queue",
|
||||
"fieldtype": "Data",
|
||||
"label": _("FIFO/LIFO Queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_queue_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(C) Total qty in queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_qty_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("A - C"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_value",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(D) Balance Stock Value"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_stock_value",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(E) Balance Stock Value in Queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_value_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("D - E"),
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(H) Valuation Rate"),
|
||||
},
|
||||
]
|
@ -111,17 +111,17 @@ def get_columns():
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Date"),
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Time"),
|
||||
},
|
||||
{
|
||||
"fieldname": "creation",
|
||||
"fieldtype": "Datetime",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Creation"),
|
||||
},
|
||||
{
|
||||
|
@ -65,6 +65,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
||||
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
||||
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
||||
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
|
||||
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
|
||||
]
|
||||
|
||||
OPTIONAL_FILTERS = {
|
||||
|
@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
datetime_limit_condition = ""
|
||||
qty_shift = args.actual_qty
|
||||
|
||||
args["time_format"] = "%H:%i:%s"
|
||||
|
||||
# find difference/shift in qty caused by stock reconciliation
|
||||
if args.voucher_type == "Stock Reconciliation":
|
||||
qty_shift = get_stock_reco_qty_shift(args)
|
||||
@ -1315,7 +1317,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
update `tabStock Ledger Entry`
|
||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||
where
|
||||
@ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
and warehouse = %(warehouse)s
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
|
||||
or (
|
||||
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and creation > %(creation)s
|
||||
)
|
||||
)
|
||||
and timestamp(posting_date, time_format(posting_time, %(time_format)s))
|
||||
> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||
{datetime_limit_condition}
|
||||
""".format(
|
||||
qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
|
||||
),
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
@ -1383,6 +1379,7 @@ def get_next_stock_reco(args):
|
||||
and creation > %(creation)s
|
||||
)
|
||||
)
|
||||
order by timestamp(posting_date, posting_time) asc, creation asc
|
||||
limit 1
|
||||
""",
|
||||
args,
|
||||
|
9
erpnext/tests/test_activation.py
Normal file
9
erpnext/tests/test_activation.py
Normal 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)
|
@ -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 '{0}' 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
@ -18,7 +18,6 @@ def get_level():
|
||||
"Customer": 5,
|
||||
"Delivery Note": 5,
|
||||
"Employee": 3,
|
||||
"Instructor": 5,
|
||||
"Issue": 5,
|
||||
"Item": 5,
|
||||
"Journal Entry": 3,
|
||||
|
Loading…
x
Reference in New Issue
Block a user