Merge branch 'develop' into feat-picklist-scan
This commit is contained in:
commit
a4f0be6c5e
@ -27,7 +27,6 @@
|
|||||||
"bank_account_no",
|
"bank_account_no",
|
||||||
"address_and_contact",
|
"address_and_contact",
|
||||||
"address_html",
|
"address_html",
|
||||||
"website",
|
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"contact_html",
|
"contact_html",
|
||||||
"integration_details_section",
|
"integration_details_section",
|
||||||
@ -156,11 +155,6 @@
|
|||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Address HTML"
|
"label": "Address HTML"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "website",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Website"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_13",
|
"fieldname": "column_break_13",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@ -208,7 +202,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-23 16:48:06.303658",
|
"modified": "2022-05-04 15:49:42.620630",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
@ -243,5 +237,6 @@
|
|||||||
"search_fields": "bank,account",
|
"search_fields": "bank,account",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -118,6 +118,7 @@ class BankClearance(Document):
|
|||||||
)
|
)
|
||||||
.where(loan_repayment.docstatus == 1)
|
.where(loan_repayment.docstatus == 1)
|
||||||
.where(loan_repayment.clearance_date.isnull())
|
.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.from_date)
|
||||||
.where(loan_repayment.posting_date <= self.to_date)
|
.where(loan_repayment.posting_date <= self.to_date)
|
||||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
.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,
|
loan_repayment.posting_date,
|
||||||
)
|
)
|
||||||
.where(loan_repayment.docstatus == 1)
|
.where(loan_repayment.docstatus == 1)
|
||||||
|
.where(loan_repayment.repay_from_salary == 0)
|
||||||
.where(loan_repayment.clearance_date.isnull())
|
.where(loan_repayment.clearance_date.isnull())
|
||||||
.where(loan_repayment.payment_account == bank_account)
|
.where(loan_repayment.payment_account == bank_account)
|
||||||
)
|
)
|
||||||
|
@ -12,7 +12,10 @@ def get_data():
|
|||||||
"Sales Invoice": "return_against",
|
"Sales Invoice": "return_against",
|
||||||
"Auto Repeat": "reference_document",
|
"Auto Repeat": "reference_document",
|
||||||
},
|
},
|
||||||
"internal_links": {"Sales Order": ["items", "sales_order"]},
|
"internal_links": {
|
||||||
|
"Sales Order": ["items", "sales_order"],
|
||||||
|
"Timesheet": ["timesheets", "time_sheet"],
|
||||||
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{
|
{
|
||||||
"label": _("Payment"),
|
"label": _("Payment"),
|
||||||
|
@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
# reset
|
# reset
|
||||||
einvoice_settings = frappe.get_doc("E Invoice Settings")
|
einvoice_settings = frappe.get_doc("E Invoice Settings")
|
||||||
einvoice_settings.enable = 0
|
einvoice_settings.enable = 0
|
||||||
|
einvoice_settings.save()
|
||||||
frappe.flags.country = country
|
frappe.flags.country = country
|
||||||
|
|
||||||
def test_einvoice_json(self):
|
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):
|
def get_tax_template(posting_date, args):
|
||||||
"""Get matching tax rule"""
|
"""Get matching tax rule"""
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
from_date = to_date = posting_date
|
conditions = []
|
||||||
if not posting_date:
|
|
||||||
from_date = "1900-01-01"
|
|
||||||
to_date = "4000-01-01"
|
|
||||||
|
|
||||||
conditions = [
|
if posting_date:
|
||||||
"""(from_date is null or from_date <= '{0}')
|
conditions.append(
|
||||||
and (to_date is null or to_date >= '{1}')""".format(
|
f"""(from_date is null or from_date <= '{posting_date}')
|
||||||
from_date, to_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(
|
conditions.append(
|
||||||
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
"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")
|
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||||
account = loan_doc.payment_account
|
account = loan_doc.payment_account
|
||||||
|
|
||||||
entries = (
|
query = (
|
||||||
frappe.qb.from_(loan_doc)
|
frappe.qb.from_(loan_doc)
|
||||||
.select(
|
.select(
|
||||||
ConstantColumn(doctype).as_("payment_document"),
|
ConstantColumn(doctype).as_("payment_document"),
|
||||||
@ -217,9 +217,12 @@ def get_loan_entries(filters):
|
|||||||
.where(account == filters.get("account"))
|
.where(account == filters.get("account"))
|
||||||
.where(posting_date <= getdate(filters.get("report_date")))
|
.where(posting_date <= getdate(filters.get("report_date")))
|
||||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > 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)
|
loan_docs.extend(entries)
|
||||||
|
|
||||||
return loan_docs
|
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)
|
gle_map[group_by_value].entries.append(gle)
|
||||||
|
|
||||||
elif group_by_voucher_consolidated:
|
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"):
|
if filters.get("include_dimensions"):
|
||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
|
@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
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}
|
p.customer, p.is_return {select_mop_field}
|
||||||
FROM
|
FROM
|
||||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||||
|
@ -114,11 +114,11 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
onload: (report) => {
|
onload: (report) => {
|
||||||
// Create a button for setting the default supplier
|
// Create a button for setting the default supplier
|
||||||
report.page.add_inner_button(__("Select 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
|
//Always make a new one so that the latest values get updated
|
||||||
reporter.make_default_supplier_dialog(report);
|
reporter.make_default_supplier_dialog(report);
|
||||||
}, 'Tools');
|
}, __("Tools"));
|
||||||
|
|
||||||
},
|
},
|
||||||
make_default_supplier_dialog: (report) => {
|
make_default_supplier_dialog: (report) => {
|
||||||
@ -126,7 +126,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
if(!report.data) return;
|
if(!report.data) return;
|
||||||
|
|
||||||
let filters = report.get_values();
|
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
|
// 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 })
|
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();
|
let values = dialog.get_values();
|
||||||
if(values) {
|
if(values) {
|
||||||
// Set the default_supplier field of the appropriate Item to the selected supplier
|
// 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
|
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_quantity(child_item, d):
|
def validate_quantity(child_item, new_data):
|
||||||
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
|
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"))
|
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"))
|
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||||
|
|
||||||
data = json.loads(trans_items)
|
data = json.loads(trans_items)
|
||||||
|
@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder import DocType
|
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.crm.utils import add_link_in_communication, copy_comments
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
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.party_name and self.opportunity_from == "Customer":
|
||||||
if self.contact_person:
|
if self.contact_person:
|
||||||
opts.description = "Contact " + cstr(self.contact_person)
|
opts.description = f"Contact {self.contact_person}"
|
||||||
else:
|
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":
|
elif self.party_name and self.opportunity_from == "Lead":
|
||||||
if self.contact_display:
|
if self.contact_display:
|
||||||
opts.description = "Contact " + cstr(self.contact_display)
|
opts.description = f"Contact {self.contact_display}"
|
||||||
else:
|
else:
|
||||||
opts.description = "Contact lead " + cstr(self.party_name)
|
opts.description = f"Contact lead {self.party_name}"
|
||||||
|
|
||||||
opts.subject = opts.description
|
opts.subject = opts.description
|
||||||
opts.description += ". By : " + cstr(self.contact_by)
|
opts.description += f". By : {self.contact_by}"
|
||||||
|
|
||||||
if self.to_discuss:
|
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)
|
super(Opportunity, self).add_calendar_event(opts, force)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
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.lead import make_customer
|
||||||
from erpnext.crm.doctype.lead.test_lead import make_lead
|
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_comment_count, 4)
|
||||||
self.assertEqual(quotation_communication_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():
|
def make_opportunity_from_lead():
|
||||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||||
|
@ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
tax_rule_master = set_taxes(
|
tax_rule_master = set_taxes(
|
||||||
quotation.party_name,
|
quotation.party_name,
|
||||||
"Customer",
|
"Customer",
|
||||||
quotation.transaction_date,
|
None,
|
||||||
quotation.company,
|
quotation.company,
|
||||||
customer_group=None,
|
customer_group=None,
|
||||||
supplier_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"
|
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_js = "erpnext.bundle.js"
|
||||||
app_include_css = "erpnext.bundle.css"
|
app_include_css = "erpnext.bundle.css"
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"default_salary_structure"
|
"default_salary_structure",
|
||||||
|
"currency",
|
||||||
|
"default_base_pay"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -16,14 +18,31 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Default Salary Structure",
|
"label": "Default Salary Structure",
|
||||||
"options": "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,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-26 13:12:07.815330",
|
"modified": "2022-05-06 15:42:10.395508",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Grade",
|
"name": "Employee Grade",
|
||||||
|
"naming_rule": "Set by user",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -65,5 +84,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"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) {
|
expire_allocation: function(frm) {
|
||||||
|
@ -254,7 +254,18 @@ class LeaveAllocation(Document):
|
|||||||
# Adding a day to include To Date in the difference
|
# Adding a day to include To Date in the difference
|
||||||
date_difference = date_diff(self.to_date, self.from_date) + 1
|
date_difference = date_diff(self.to_date, self.from_date) + 1
|
||||||
if date_difference < self.total_leaves_allocated:
|
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):
|
def create_leave_ledger_entry(self, submit=True):
|
||||||
if self.unused_leaves:
|
if self.unused_leaves:
|
||||||
|
@ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(frappe.ValidationError, doc.save)
|
||||||
|
|
||||||
def test_validation_for_over_allocation(self):
|
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(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": self.employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": self.employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": leave_type.name,
|
||||||
"from_date": getdate("2015-09-1"),
|
"from_date": getdate("2015-09-1"),
|
||||||
"to_date": getdate("2015-09-30"),
|
"to_date": getdate("2015-09-30"),
|
||||||
"new_leaves_allocated": 35,
|
"new_leaves_allocated": 35,
|
||||||
|
"carry_forward": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# allocated leave more than period
|
# allocated leave more than period
|
||||||
self.assertRaises(OverAllocationError, doc.save)
|
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):
|
def test_validation_for_over_allocation_post_submission(self):
|
||||||
allocation = frappe.get_doc(
|
allocation = frappe.get_doc(
|
||||||
{
|
{
|
||||||
|
@ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while i < 14:
|
while i < 14:
|
||||||
allocate_earned_leaves(ignore_duplicates=True)
|
allocate_earned_leaves()
|
||||||
i += 1
|
i += 1
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
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)
|
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||||
i = 0
|
i = 0
|
||||||
while i < 6:
|
while i < 6:
|
||||||
allocate_earned_leaves(ignore_duplicates=True)
|
allocate_earned_leaves()
|
||||||
i += 1
|
i += 1
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
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 (
|
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"]
|
test_dependencies = ["Employee"]
|
||||||
|
|
||||||
|
|
||||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
class TestLeavePolicyAssignment(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for doctype in [
|
for doctype in [
|
||||||
"Leave Period",
|
"Leave Period",
|
||||||
@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
leave_policy = create_leave_policy()
|
leave_policy = create_leave_policy()
|
||||||
leave_policy.submit()
|
leave_policy.submit()
|
||||||
|
|
||||||
|
self.employee.date_of_joining = get_first_day(leave_period.from_date)
|
||||||
|
self.employee.save()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"assignment_based_on": "Leave Period",
|
"assignment_based_on": "Leave Period",
|
||||||
"leave_policy": leave_policy.name,
|
"leave_policy": leave_policy.name,
|
||||||
@ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
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):
|
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
|
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.unused_leaves, 5)
|
||||||
self.assertEqual(details.total_leaves_allocated, 7)
|
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):
|
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
|
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
|
||||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
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(effective_from, self.employee.date_of_joining)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
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):
|
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
|
# 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(
|
leave_period, leave_policy = setup_leave_period_and_policy(
|
||||||
@ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
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):
|
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
|
# 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)
|
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(effective_from, self.employee.date_of_joining)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
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):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||||
frappe.flags.current_date = None
|
frappe.flags.current_date = None
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"fraction_of_daily_salary_per_leave",
|
"fraction_of_daily_salary_per_leave",
|
||||||
"is_optional_leave",
|
"is_optional_leave",
|
||||||
"allow_negative",
|
"allow_negative",
|
||||||
|
"allow_over_allocation",
|
||||||
"include_holiday",
|
"include_holiday",
|
||||||
"is_compensatory",
|
"is_compensatory",
|
||||||
"carry_forward_section",
|
"carry_forward_section",
|
||||||
@ -211,15 +212,23 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Fraction of Daily Salary per Leave",
|
"label": "Fraction of Daily Salary per Leave",
|
||||||
"mandatory_depends_on": "eval:doc.is_ppl == 1"
|
"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",
|
"icon": "fa fa-flag",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-02 11:59:40.503359",
|
"modified": "2022-05-09 05:01:38.957545",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Type",
|
"name": "Leave Type",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -251,5 +260,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -269,7 +269,7 @@ def generate_leave_encashment():
|
|||||||
create_leave_encashment(leave_allocation=leave_allocation)
|
create_leave_encashment(leave_allocation=leave_allocation)
|
||||||
|
|
||||||
|
|
||||||
def allocate_earned_leaves(ignore_duplicates=False):
|
def allocate_earned_leaves():
|
||||||
"""Allocate earned leaves to Employees"""
|
"""Allocate earned leaves to Employees"""
|
||||||
e_leave_types = get_earned_leaves()
|
e_leave_types = get_earned_leaves()
|
||||||
today = getdate()
|
today = getdate()
|
||||||
@ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False):
|
|||||||
if check_effective_date(
|
if check_effective_date(
|
||||||
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
|
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
|
||||||
):
|
):
|
||||||
update_previous_leave_allocation(
|
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
|
||||||
allocation, annual_allocation, e_leave_type, ignore_duplicates
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_previous_leave_allocation(
|
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||||
allocation, annual_allocation, e_leave_type, ignore_duplicates=False
|
|
||||||
):
|
|
||||||
earned_leaves = get_monthly_earned_leave(
|
earned_leaves = get_monthly_earned_leave(
|
||||||
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
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:
|
if new_allocation != allocation.total_leaves_allocated:
|
||||||
today_date = today()
|
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)
|
||||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
|
||||||
|
|
||||||
if e_leave_type.based_on_date_of_joining:
|
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(
|
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))
|
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
||||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
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):
|
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.v13_0.change_default_item_manufacturer_fieldtype
|
||||||
erpnext.patches.v14_0.discount_accounting_separation
|
erpnext.patches.v14_0.discount_accounting_separation
|
||||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
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')
|
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();
|
d.show();
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_years, date_diff, get_first_day, nowdate
|
from frappe.utils import add_years, date_diff, get_first_day, nowdate
|
||||||
from frappe.utils.make_random import get_random
|
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"]
|
test_dependencies = ["Fiscal Year"]
|
||||||
|
|
||||||
|
|
||||||
class TestSalaryStructure(unittest.TestCase):
|
class TestSalaryStructure(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]:
|
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]:
|
||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
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.base, 5000)
|
||||||
self.assertEqual(salary_structure_assignment.variable, 200)
|
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):
|
def test_multi_currency_salary_structure(self):
|
||||||
make_employee("test_muti_currency_employee@salary.com")
|
make_employee("test_muti_currency_employee@salary.com")
|
||||||
sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency="USD")
|
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:
|
if not company:
|
||||||
company = erpnext.get_default_company()
|
company = erpnext.get_default_company()
|
||||||
return frappe.db.get_value("Company", company, "default_payroll_payable_account")
|
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",
|
||||||
"employee_name",
|
"employee_name",
|
||||||
"department",
|
"department",
|
||||||
|
"grade",
|
||||||
"company",
|
"company",
|
||||||
"payroll_payable_account",
|
"payroll_payable_account",
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
@ -67,6 +68,8 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "grade.default_salary_structure",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "salary_structure",
|
"fieldname": "salary_structure",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -96,6 +99,8 @@
|
|||||||
"label": "Base & Variable"
|
"label": "Base & Variable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "grade.default_base_pay",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "base",
|
"fieldname": "base",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Base",
|
"label": "Base",
|
||||||
@ -158,11 +163,19 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Cost Centers",
|
"label": "Cost Centers",
|
||||||
"options": "Employee Cost Center"
|
"options": "Employee Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "employee.grade",
|
||||||
|
"fieldname": "grade",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Grade",
|
||||||
|
"options": "Employee Grade",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-19 12:43:54.439073",
|
"modified": "2022-05-06 12:18:36.972336",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Structure Assignment",
|
"name": "Salary Structure Assignment",
|
||||||
|
@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
...data
|
...data
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: () => frm.reload_doc() || d.hide(),
|
callback: () => {
|
||||||
error: () => d.hide()
|
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')
|
primary_action_label: __('Submit')
|
||||||
@ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
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 = () => {
|
const action = () => {
|
||||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
const d = new frappe.ui.Dialog({
|
||||||
message += '<br><br>';
|
title: __('Cancel E-Way Bill'),
|
||||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
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({
|
const dialog = frappe.msgprint({
|
||||||
title: __('Update E-Way Bill Cancelled Status?'),
|
title: __("Generate QRCode"),
|
||||||
message: message,
|
message: __("Generate and attach QR Code using IRN?"),
|
||||||
indicator: 'orange',
|
|
||||||
primary_action: {
|
primary_action: {
|
||||||
action: function() {
|
action: function() {
|
||||||
frappe.call({
|
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 },
|
args: { doctype, docname: name },
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: () => frm.reload_doc() || dialog.hide()
|
callback: () => frm.reload_doc() || dialog.hide(),
|
||||||
|
error: () => dialog.hide()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primary_action_label: __('Yes')
|
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) => {
|
const get_ewaybill_fields = (frm) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter',
|
fieldname: "eway_part_a_section_break",
|
||||||
'label': 'Transporter',
|
fieldtype: "Section Break",
|
||||||
'fieldtype': 'Link',
|
label: "Part A",
|
||||||
'options': 'Supplier',
|
|
||||||
'default': frm.doc.transporter
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'gst_transporter_id',
|
fieldname: "transporter",
|
||||||
'label': 'GST Transporter ID',
|
label: "Transporter",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Link",
|
||||||
'default': frm.doc.gst_transporter_id
|
options: "Supplier",
|
||||||
|
default: frm.doc.transporter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'driver',
|
fieldname: "transporter_name",
|
||||||
'label': 'Driver',
|
label: "Transporter Name",
|
||||||
'fieldtype': 'Link',
|
fieldtype: "Data",
|
||||||
'options': 'Driver',
|
read_only: 1,
|
||||||
'default': frm.doc.driver
|
default: frm.doc.transporter_name,
|
||||||
|
depends_on: "transporter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'lr_no',
|
fieldname: "part_a_column_break",
|
||||||
'label': 'Transport Receipt No',
|
fieldtype: "Column Break",
|
||||||
'fieldtype': 'Data',
|
|
||||||
'default': frm.doc.lr_no
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'vehicle_no',
|
fieldname: "gst_transporter_id",
|
||||||
'label': 'Vehicle No',
|
label: "GST Transporter ID",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Data",
|
||||||
'default': frm.doc.vehicle_no
|
default: frm.doc.gst_transporter_id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'distance',
|
fieldname: "distance",
|
||||||
'label': 'Distance (in km)',
|
label: "Distance (in km)",
|
||||||
'fieldtype': 'Float',
|
fieldtype: "Float",
|
||||||
'default': frm.doc.distance
|
default: frm.doc.distance,
|
||||||
|
description: 'Set as zero to auto calculate distance using pin codes',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter_col_break',
|
fieldname: "eway_part_b_section_break",
|
||||||
'fieldtype': 'Column Break',
|
fieldtype: "Section Break",
|
||||||
|
label: "Part B",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter_name',
|
fieldname: "mode_of_transport",
|
||||||
'label': 'Transporter Name',
|
label: "Mode of Transport",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Select",
|
||||||
'read_only': 1,
|
options: `\nRoad\nAir\nRail\nShip`,
|
||||||
'default': frm.doc.transporter_name,
|
default: frm.doc.mode_of_transport,
|
||||||
'depends_on': 'transporter'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'mode_of_transport',
|
fieldname: "gst_vehicle_type",
|
||||||
'label': 'Mode of Transport',
|
label: "GST Vehicle Type",
|
||||||
'fieldtype': 'Select',
|
fieldtype: "Select",
|
||||||
'options': `\nRoad\nAir\nRail\nShip`,
|
options: `Regular\nOver Dimensional Cargo (ODC)`,
|
||||||
'default': frm.doc.mode_of_transport
|
depends_on: 'eval:(doc.mode_of_transport === "Road")',
|
||||||
|
default: frm.doc.gst_vehicle_type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'driver_name',
|
fieldname: "vehicle_no",
|
||||||
'label': 'Driver Name',
|
label: "Vehicle No",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Data",
|
||||||
'fetch_from': 'driver.full_name',
|
default: frm.doc.vehicle_no,
|
||||||
'read_only': 1,
|
|
||||||
'default': frm.doc.driver_name,
|
|
||||||
'depends_on': 'driver'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'lr_date',
|
fieldname: "part_b_column_break",
|
||||||
'label': 'Transport Receipt Date',
|
fieldtype: "Column Break",
|
||||||
'fieldtype': 'Date',
|
|
||||||
'default': frm.doc.lr_date
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'gst_vehicle_type',
|
fieldname: "lr_date",
|
||||||
'label': 'GST Vehicle Type',
|
label: "Transport Receipt Date",
|
||||||
'fieldtype': 'Select',
|
fieldtype: "Date",
|
||||||
'options': `Regular\nOver Dimensional Cargo (ODC)`,
|
default: frm.doc.lr_date,
|
||||||
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
},
|
||||||
'default': frm.doc.gst_vehicle_type
|
{
|
||||||
}
|
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"),
|
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_name = invoice.name
|
||||||
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
|
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
|
||||||
@ -443,7 +448,7 @@ def get_eway_bill_details(invoice):
|
|||||||
dict(
|
dict(
|
||||||
gstin=invoice.gst_transporter_id,
|
gstin=invoice.gst_transporter_id,
|
||||||
name=invoice.transporter_name,
|
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,
|
distance=invoice.distance or 0,
|
||||||
document_name=invoice.lr_no,
|
document_name=invoice.lr_no,
|
||||||
document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
|
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.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
|
||||||
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
||||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
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.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):
|
def set_invoice(self):
|
||||||
self.invoice = None
|
self.invoice = None
|
||||||
@ -857,8 +863,8 @@ class GSPConnector:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def auto_refresh_token(self):
|
def auto_refresh_token(self):
|
||||||
self.fetch_auth_token()
|
|
||||||
self.token_auto_refreshed = True
|
self.token_auto_refreshed = True
|
||||||
|
self.fetch_auth_token()
|
||||||
|
|
||||||
def log_request(self, url, headers, data, res):
|
def log_request(self, url, headers, data, res):
|
||||||
headers.update({"password": self.credentials.password})
|
headers.update({"password": self.credentials.password})
|
||||||
@ -998,6 +1004,37 @@ class GSPConnector:
|
|||||||
|
|
||||||
return failed
|
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):
|
def get_irn_details(self, irn):
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
|
|
||||||
@ -1113,6 +1150,19 @@ class GSPConnector:
|
|||||||
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
|
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
|
||||||
self.invoice.eway_bill_cancelled = 0
|
self.invoice.eway_bill_cancelled = 0
|
||||||
self.invoice.update(args)
|
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 = {
|
self.invoice.flags.updater_reference = {
|
||||||
"doctype": self.invoice.doctype,
|
"doctype": self.invoice.doctype,
|
||||||
"docname": self.invoice.name,
|
"docname": self.invoice.name,
|
||||||
@ -1135,7 +1185,6 @@ class GSPConnector:
|
|||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||||
headers["username"] = headers["user_name"]
|
headers["username"] = headers["user_name"]
|
||||||
del headers["user_name"]
|
|
||||||
try:
|
try:
|
||||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||||
if res.get("success"):
|
if res.get("success"):
|
||||||
@ -1186,8 +1235,6 @@ class GSPConnector:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
def raise_error(self, raise_exception=False, errors=None):
|
def raise_error(self, raise_exception=False, errors=None):
|
||||||
if errors is None:
|
|
||||||
errors = []
|
|
||||||
title = _("E Invoice Request Failed")
|
title = _("E Invoice Request Failed")
|
||||||
if errors:
|
if errors:
|
||||||
frappe.throw(errors, title=title, as_list=1)
|
frappe.throw(errors, title=title, as_list=1)
|
||||||
@ -1228,13 +1275,18 @@ class GSPConnector:
|
|||||||
|
|
||||||
def attach_qrcode_image(self):
|
def attach_qrcode_image(self):
|
||||||
qrcode = self.invoice.signed_qr_code
|
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()
|
qr_image = io.BytesIO()
|
||||||
url = qrcreate(qrcode, error="L")
|
url = qrcreate(qrcode, error="L")
|
||||||
url.png(qr_image, scale=2, quiet_zone=1)
|
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(
|
_file = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "File",
|
"doctype": "File",
|
||||||
@ -1243,12 +1295,12 @@ class GSPConnector:
|
|||||||
"attached_to_name": docname,
|
"attached_to_name": docname,
|
||||||
"attached_to_field": "qrcode_image",
|
"attached_to_field": "qrcode_image",
|
||||||
"is_private": 0,
|
"is_private": 0,
|
||||||
"content": qr_image.getvalue(),
|
"content": qr_image,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_file.save()
|
_file.save()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
self.invoice.qrcode_image = _file.file_url
|
return _file
|
||||||
|
|
||||||
def update_invoice(self):
|
def update_invoice(self):
|
||||||
self.invoice.flags.ignore_validate_update_after_submit = True
|
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)
|
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()
|
@frappe.whitelist()
|
||||||
def generate_eway_bill(doctype, docname, **kwargs):
|
def generate_eway_bill(doctype, docname, **kwargs):
|
||||||
gsp_connector = GSPConnector(doctype, docname)
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
@ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_eway_bill(doctype, docname):
|
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||||
# TODO: uncomment when eway_bill api from Adequare is enabled
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
# gsp_connector = GSPConnector(doctype, docname)
|
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -32,7 +32,7 @@ def _execute(filters=None):
|
|||||||
added_item = []
|
added_item = []
|
||||||
for d in item_list:
|
for d in item_list:
|
||||||
if (d.parent, d.item_code) not in added_item:
|
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
|
total_tax = 0
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
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 + total_tax]
|
||||||
row += [d.base_net_amount]
|
row += [d.base_net_amount]
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||||
row += [item_tax.get("tax_amount", 0)]
|
row += [item_tax.get("tax_amount", 0)]
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
added_item.append((d.parent, d.item_code))
|
added_item.append((d.parent, d.item_code))
|
||||||
if data:
|
if data:
|
||||||
@ -64,6 +62,7 @@ def get_columns():
|
|||||||
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
|
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
|
||||||
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
|
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
|
||||||
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
|
{"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": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
|
||||||
{
|
{
|
||||||
"fieldname": "taxable_amount",
|
"fieldname": "taxable_amount",
|
||||||
@ -106,16 +105,25 @@ def get_items(filters):
|
|||||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
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_net_amount) as base_net_amount,
|
||||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
`tabSales Invoice Item`.parent,
|
||||||
`tabGST HSN Code`.description
|
`tabSales Invoice Item`.item_code,
|
||||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
`tabGST HSN Code`.description,
|
||||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
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`.docstatus = 1
|
||||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||||
group by
|
group by
|
||||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
|
`tabSales Invoice Item`.parent,
|
||||||
|
`tabSales Invoice Item`.item_code
|
||||||
"""
|
"""
|
||||||
% (conditions, match_conditions),
|
% (conditions, match_conditions),
|
||||||
filters,
|
filters,
|
||||||
@ -213,15 +221,16 @@ def get_merged_data(columns, data):
|
|||||||
result = []
|
result = []
|
||||||
|
|
||||||
for row in data:
|
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):
|
for i, d in enumerate(columns):
|
||||||
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
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:
|
else:
|
||||||
if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""):
|
if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""):
|
||||||
merged_hsn_dict[row[0]][d["fieldname"]] += row[i]
|
merged_hsn_dict[key][d["fieldname"]] += row[i]
|
||||||
else:
|
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():
|
for key, value in merged_hsn_dict.items():
|
||||||
result.append(value)
|
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)
|
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)}
|
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"),
|
"desc": hsn.get("description"),
|
||||||
"uqc": hsn.get("stock_uom").upper(),
|
"uqc": hsn.get("stock_uom").upper(),
|
||||||
"qty": hsn.get("stock_qty"),
|
"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)),
|
"txval": flt(hsn.get("taxable_amount", 2)),
|
||||||
"iamt": 0.0,
|
"iamt": 0.0,
|
||||||
"camt": 0.0,
|
"camt": 0.0,
|
||||||
|
@ -65,7 +65,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.set_value('transaction_date', frappe.datetime.get_today())
|
frm.set_value('transaction_date', frappe.datetime.get_today())
|
||||||
}
|
}
|
||||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
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) {
|
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'];
|
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
|
||||||
},
|
},
|
||||||
|
@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
this.frm = this.get_new_frm(this.frm);
|
this.frm = this.get_new_frm(this.frm);
|
||||||
this.frm.doc.items = [];
|
this.frm.doc.items = [];
|
||||||
const res = await frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||||
args: {
|
args: {
|
||||||
'source_name': doc.name,
|
'source_name': doc.name,
|
||||||
'target_doc': this.frm.doc
|
'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() {
|
set_pos_profile_data() {
|
||||||
|
@ -238,4 +238,5 @@ def get_chart_data(data):
|
|||||||
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
|
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
|
||||||
},
|
},
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
|
|||||||
},
|
},
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -415,3 +415,8 @@ class Analytics(object):
|
|||||||
else:
|
else:
|
||||||
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
||||||
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
|
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",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("Naming Series", {
|
frappe.ui.form.on("Naming Series", {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.disable_save();
|
|
||||||
frm.events.get_doc_and_prefix(frm);
|
frm.events.get_doc_and_prefix(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frm.disable_save();
|
||||||
|
},
|
||||||
|
|
||||||
get_doc_and_prefix: function(frm) {
|
get_doc_and_prefix: function(frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_transactions",
|
method: "get_transactions",
|
||||||
|
@ -962,6 +962,44 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
automatically_fetch_payment_terms(enable=0)
|
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):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
@ -737,7 +737,9 @@
|
|||||||
"depends_on": "returned_qty",
|
"depends_on": "returned_qty",
|
||||||
"fieldname": "returned_qty",
|
"fieldname": "returned_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Returned Qty in Stock UOM"
|
"label": "Returned Qty in Stock UOM",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "incoming_rate",
|
"fieldname": "incoming_rate",
|
||||||
@ -778,7 +780,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-31 18:36:24.671913",
|
"modified": "2022-05-02 12:09:39.610075",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"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.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
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.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 (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
@ -180,9 +183,12 @@ def make_items():
|
|||||||
if not frappe.db.exists("Item", item_code):
|
if not frappe.db.exists("Item", item_code):
|
||||||
create_item(item_code)
|
create_item(item_code)
|
||||||
|
|
||||||
create_stock_reconciliation(
|
try:
|
||||||
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
|
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"):
|
if frappe.db.exists("Item", "Test FG A RW 1"):
|
||||||
doc = frappe.get_doc("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]
|
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||||
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
|
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):
|
def test_warehouse_company_validation(self):
|
||||||
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
||||||
frappe.get_doc("User", "test2@example.com").add_roles(
|
frappe.get_doc("User", "test2@example.com").add_roles(
|
||||||
|
@ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
backdated.cancel()
|
backdated.cancel()
|
||||||
self.assertEqual([1], ordered_qty_after_transaction())
|
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):
|
def create_repack_entry(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -62,6 +62,7 @@ class StockReconciliation(StockController):
|
|||||||
self.make_sle_on_cancel()
|
self.make_sle_on_cancel()
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
def remove_items_with_no_change(self):
|
def remove_items_with_no_change(self):
|
||||||
"""Remove items if qty or rate is not changed"""
|
"""Remove items if qty or rate is not changed"""
|
||||||
@ -456,7 +457,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
key = (d.item_code, d.warehouse)
|
key = (d.item_code, d.warehouse)
|
||||||
if key not in merge_similar_entries:
|
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
|
merge_similar_entries[key] = d
|
||||||
elif d.serial_no:
|
elif d.serial_no:
|
||||||
data = merge_similar_entries[key]
|
data = merge_similar_entries[key]
|
||||||
|
@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.local.future_sle = {}
|
frappe.local.future_sle = {}
|
||||||
|
frappe.flags.pop("dont_execute_stock_reposts", None)
|
||||||
|
|
||||||
def test_reco_for_fifo(self):
|
def test_reco_for_fifo(self):
|
||||||
self._test_reco_sle_gle("FIFO")
|
self._test_reco_sle_gle("FIFO")
|
||||||
@ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
sr = create_stock_reconciliation(
|
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.save()
|
||||||
sr.submit()
|
sr.submit()
|
||||||
@ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||||
stock_doc.cancel()
|
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):
|
def test_customer_provided_items(self):
|
||||||
item_code = "Stock-Reco-customer-Item-100"
|
item_code = "Stock-Reco-customer-Item-100"
|
||||||
create_item(
|
create_item(
|
||||||
@ -306,6 +385,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
Var | Doc | Qty | Balance
|
Var | Doc | Qty | Balance
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
PR5 | PR | 10 | 10 (posting date: today-4) [backdated]
|
||||||
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
|
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
|
||||||
PR1 | PR | 10 | 18 (posting date: today-3)
|
PR1 | PR | 10 | 18 (posting date: today-3)
|
||||||
PR2 | PR | 1 | 19 (posting date: today-2)
|
PR2 | PR | 1 | 19 (posting date: today-2)
|
||||||
@ -315,6 +395,14 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
item_code = make_item().name
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
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(
|
pr1 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
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(
|
pr3 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
|
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
|
||||||
)
|
)
|
||||||
|
assertBalance(pr1, 10)
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr3, 12)
|
||||||
"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)
|
|
||||||
|
|
||||||
# post backdated stock reco in between
|
# post backdated stock reco in between
|
||||||
sr4 = create_stock_reconciliation(
|
sr4 = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
|
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
|
||||||
)
|
)
|
||||||
pr3_balance = frappe.db.get_value(
|
assertBalance(pr3, 7)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
self.assertEqual(pr3_balance, 7)
|
|
||||||
|
|
||||||
# post backdated stock reco at the start
|
# post backdated stock reco at the start
|
||||||
sr5 = create_stock_reconciliation(
|
sr5 = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
|
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
|
||||||
)
|
)
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr1, 18)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
|
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(
|
assertBalance(pr5, 10)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
|
# check if future stock reco is unaffected
|
||||||
)
|
assertBalance(sr4, 6)
|
||||||
sr4_balance = frappe.db.get_value(
|
assertBalance(sr5, 8)
|
||||||
"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
|
|
||||||
|
|
||||||
# cancel backdated stock reco and check future impact
|
# cancel backdated stock reco and check future impact
|
||||||
sr5.cancel()
|
sr5.cancel()
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr1, 10)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
|
assertBalance(pr2, 11)
|
||||||
)
|
assertBalance(sr4, 6) # check if future stock reco is unaffected
|
||||||
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()
|
|
||||||
|
|
||||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
def test_backdated_stock_reco_future_negative_stock(self):
|
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
|
# repost will make this test useless, qty should update in realtime without reposts
|
||||||
frappe.flags.dont_execute_stock_reposts = True
|
frappe.flags.dont_execute_stock_reposts = True
|
||||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
|
||||||
|
|
||||||
item_code = make_item().name
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
@ -684,11 +746,13 @@ def create_stock_reconciliation(**args):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if not args.do_not_save:
|
||||||
if not args.do_not_submit:
|
sr.insert()
|
||||||
sr.submit()
|
try:
|
||||||
except EmptyStockReconciliationItemsError:
|
if not args.do_not_submit:
|
||||||
pass
|
sr.submit()
|
||||||
|
except EmptyStockReconciliationItemsError:
|
||||||
|
pass
|
||||||
return sr
|
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",
|
"fieldname": "posting_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Data",
|
||||||
"label": _("Posting Date"),
|
"label": _("Posting Date"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "posting_time",
|
"fieldname": "posting_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Data",
|
||||||
"label": _("Posting Time"),
|
"label": _("Posting Time"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "creation",
|
"fieldname": "creation",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Data",
|
||||||
"label": _("Creation"),
|
"label": _("Creation"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,6 +65,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
|||||||
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
||||||
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
||||||
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
("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 = {
|
OPTIONAL_FILTERS = {
|
||||||
|
@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
datetime_limit_condition = ""
|
datetime_limit_condition = ""
|
||||||
qty_shift = args.actual_qty
|
qty_shift = args.actual_qty
|
||||||
|
|
||||||
|
args["time_format"] = "%H:%i:%s"
|
||||||
|
|
||||||
# find difference/shift in qty caused by stock reconciliation
|
# find difference/shift in qty caused by stock reconciliation
|
||||||
if args.voucher_type == "Stock Reconciliation":
|
if args.voucher_type == "Stock Reconciliation":
|
||||||
qty_shift = get_stock_reco_qty_shift(args)
|
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)
|
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||||
|
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""
|
f"""
|
||||||
update `tabStock Ledger Entry`
|
update `tabStock Ledger Entry`
|
||||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||||
where
|
where
|
||||||
@ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and voucher_no != %(voucher_no)s
|
and voucher_no != %(voucher_no)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
|
and timestamp(posting_date, time_format(posting_time, %(time_format)s))
|
||||||
or (
|
> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||||
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
|
|
||||||
and creation > %(creation)s
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{datetime_limit_condition}
|
{datetime_limit_condition}
|
||||||
""".format(
|
""",
|
||||||
qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
|
|
||||||
),
|
|
||||||
args,
|
args,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1383,6 +1379,7 @@ def get_next_stock_reco(args):
|
|||||||
and creation > %(creation)s
|
and creation > %(creation)s
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
order by timestamp(posting_date, posting_time) asc, creation asc
|
||||||
limit 1
|
limit 1
|
||||||
""",
|
""",
|
||||||
args,
|
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 Settings,Kontoauszug Einstellungen,
|
||||||
Bank Statement balance as per General Ledger,Kontoauszug Bilanz nach Hauptbuch,
|
Bank Statement balance as per General Ledger,Kontoauszug Bilanz nach Hauptbuch,
|
||||||
Bank account cannot be named as {0},Bankname {0} ungültig,
|
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,Bankwesen,
|
||||||
Banking and Payments,Bank- und Zahlungsverkehr,
|
Banking and Payments,Bank- und Zahlungsverkehr,
|
||||||
Barcode {0} already used in Item {1},Barcode {0} wird bereits für Artikel {1} verwendet,
|
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 Delivery Note,Von Lieferschein,
|
||||||
From Fiscal Year,Ab dem Geschäftsjahr,
|
From Fiscal Year,Ab dem Geschäftsjahr,
|
||||||
From GSTIN,Von GSTIN,
|
From GSTIN,Von GSTIN,
|
||||||
From Party Name,Von Party Name,
|
From Party Name,Name des Absenders,
|
||||||
From Pin Code,Von Pin-Code,
|
From Pin Code,Von Pin-Code,
|
||||||
From Place,Von Ort,
|
From Place,Von Ort,
|
||||||
From Range has to be less than To Range,Von-Bereich muss kleiner sein als Bis-Bereich,
|
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,Bruttokaufbetrag,
|
||||||
Gross Purchase Amount is mandatory,Bruttokaufbetrag ist erforderlich,
|
Gross Purchase Amount is mandatory,Bruttokaufbetrag ist erforderlich,
|
||||||
Group by Account,Gruppieren nach Konto,
|
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,Gruppieren nach Beleg,
|
||||||
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
|
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,
|
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,
|
Part-time,Teilzeit,
|
||||||
Partially Depreciated,Teilweise abgeschrieben,
|
Partially Depreciated,Teilweise abgeschrieben,
|
||||||
Partially Received,Teilweise erhalten,
|
Partially Received,Teilweise erhalten,
|
||||||
Party,Gruppe,
|
Party,Partei,
|
||||||
Party Name,Name,
|
Party Name,Name der Partei,
|
||||||
Party Type,Gruppen-Typ,
|
Party Type,Partei-Typ,
|
||||||
Party Type and Party is mandatory for {0} account,Party Type und Party ist für das Konto {0} obligatorisch,
|
Party Type and Party is mandatory for {0} account,Partei-Typ und Partei sind Pflichtfelder für Konto {0},
|
||||||
Party Type is mandatory,Party-Typ ist Pflicht,
|
Party Type is mandatory,Partei-Typ ist ein Pflichtfeld,
|
||||||
Party is mandatory,Partei ist obligatorisch,
|
Party is mandatory,Partei ist ein Pflichtfeld,
|
||||||
Password,Passwort,
|
Password,Passwort,
|
||||||
Password policy for Salary Slips is not set,Die Kennwortrichtlinie für Gehaltsabrechnungen ist nicht festgelegt,
|
Password policy for Salary Slips is not set,Die Kennwortrichtlinie für Gehaltsabrechnungen ist nicht festgelegt,
|
||||||
Past Due Date,Fälligkeitsdatum,
|
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 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 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 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,Bitte einen Patienten auswählen,
|
||||||
Please select Patient to get Lab Tests,"Bitte wählen Sie Patient, um Labortests zu erhalten",
|
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 Posting Date first,Bitte zuerst ein Buchungsdatum auswählen,
|
||||||
Please select Price List,Bitte eine Preisliste auswählen,
|
Please select Price List,Bitte eine Preisliste auswählen,
|
||||||
Please select Program,Bitte wählen Sie Programm,
|
Please select Program,Bitte wählen Sie Programm,
|
||||||
@ -2184,7 +2184,7 @@ Process Day Book Data,Tagesbuchdaten verarbeiten,
|
|||||||
Process Master Data,Stammdaten bearbeiten,
|
Process Master Data,Stammdaten bearbeiten,
|
||||||
Processing Chart of Accounts and Parties,Verarbeiten des Kontenplans und der Parteien,
|
Processing Chart of Accounts and Parties,Verarbeiten des Kontenplans und der Parteien,
|
||||||
Processing Items and UOMs,Verarbeiten von Artikeln und Mengeneinheiten,
|
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,
|
Processing Vouchers,Bearbeitung von Gutscheinen,
|
||||||
Procurement,Beschaffung,
|
Procurement,Beschaffung,
|
||||||
Produced Qty,Produzierte Menge,
|
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}: 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}: 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}: 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 / 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}: Gruppen-Typ und Gruppe sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
|
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}: 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 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,
|
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 Deliver and Bill,Auszuliefern und Abzurechnen,
|
||||||
To Fiscal Year,Bis zum Geschäftsjahr,
|
To Fiscal Year,Bis zum Geschäftsjahr,
|
||||||
To GSTIN,Zu GSTIN,
|
To GSTIN,Zu GSTIN,
|
||||||
To Party Name,Zum Party-Namen,
|
To Party Name,Name des Empfängers,
|
||||||
To Pin Code,PIN-Code,
|
To Pin Code,PIN-Code,
|
||||||
To Place,Hinstellen,
|
To Place,Hinstellen,
|
||||||
To Receive,Zu empfangen,
|
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 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 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 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 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 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.,
|
To make Customer based incentive schemes.,Um Kunden basierte Anreizsysteme zu machen.,
|
||||||
@ -4550,7 +4550,7 @@ Company Account,Firmenkonto,
|
|||||||
Account Subtype,Kontosubtyp,
|
Account Subtype,Kontosubtyp,
|
||||||
Is Default Account,Ist Standardkonto,
|
Is Default Account,Ist Standardkonto,
|
||||||
Is Company Account,Ist Unternehmenskonto,
|
Is Company Account,Ist Unternehmenskonto,
|
||||||
Party Details,Party Details,
|
Party Details,Details der Partei,
|
||||||
Account Details,Kontendaten,
|
Account Details,Kontendaten,
|
||||||
IBAN,IBAN,
|
IBAN,IBAN,
|
||||||
Bank Account No,Bankkonto Nr,
|
Bank Account No,Bankkonto Nr,
|
||||||
@ -4786,9 +4786,9 @@ Payment Order,Zahlungsauftrag,
|
|||||||
Subscription Section,Abonnementbereich,
|
Subscription Section,Abonnementbereich,
|
||||||
Journal Entry Account,Journalbuchungskonto,
|
Journal Entry Account,Journalbuchungskonto,
|
||||||
Account Balance,Kontostand,
|
Account Balance,Kontostand,
|
||||||
Party Balance,Gruppen-Saldo,
|
Party Balance,Saldo der Partei,
|
||||||
Accounting Dimensions,Abrechnungsdimensionen,
|
Accounting Dimensions,Abrechnungsdimensionen,
|
||||||
If Income or Expense,Wenn Ertrag oder Aufwand,
|
If Income or Expense,Wenn Ertrag oder Aufwand,
|
||||||
Exchange Rate,Wechselkurs,
|
Exchange Rate,Wechselkurs,
|
||||||
Debit in Company Currency,Soll in Unternehmenswährung,
|
Debit in Company Currency,Soll in Unternehmenswährung,
|
||||||
Credit in Company Currency,(Gut)Haben 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 Percentages,Prozentuale Aufteilungen der monatsweisen Verteilung,
|
||||||
Monthly Distribution Percentage,Prozentuale Aufteilung der monatsweisen Verteilung,
|
Monthly Distribution Percentage,Prozentuale Aufteilung der monatsweisen Verteilung,
|
||||||
Percentage Allocation,Prozentuale Aufteilung,
|
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.,
|
Create missing customer or supplier.,Erstelle einen fehlenden Kunden oder Lieferanten.,
|
||||||
Opening Invoice Creation Tool Item,Eröffnen des Rechnungserstellungswerkzeugs,
|
Opening Invoice Creation Tool Item,Eröffnen des Rechnungserstellungswerkzeugs,
|
||||||
Temporary Opening Account,Temporäres Eröffnungskonto,
|
Temporary Opening Account,Temporäres Eröffnungskonto,
|
||||||
Party Account,Gruppenkonto,
|
Party Account,Konto der Partei,
|
||||||
Type of Payment,Zahlungsart,
|
Type of Payment,Zahlungsart,
|
||||||
ACC-PAY-.YYYY.-,ACC-PAY-.JJJJ.-,
|
ACC-PAY-.YYYY.-,ACC-PAY-.JJJJ.-,
|
||||||
Receive,Empfangen,
|
Receive,Empfangen,
|
||||||
@ -4842,7 +4842,7 @@ Payment Order Status,Zahlungsauftragsstatus,
|
|||||||
Payment Ordered,Zahlung bestellt,
|
Payment Ordered,Zahlung bestellt,
|
||||||
Payment From / To,Zahlung von / an,
|
Payment From / To,Zahlung von / an,
|
||||||
Company Bank Account,Firmenkonto,
|
Company Bank Account,Firmenkonto,
|
||||||
Party Bank Account,Party-Bankkonto,
|
Party Bank Account,Bankkonto der Partei,
|
||||||
Account Paid From,Ausgangskonto,
|
Account Paid From,Ausgangskonto,
|
||||||
Account Paid To,Eingangskonto,
|
Account Paid To,Eingangskonto,
|
||||||
Paid Amount (Company Currency),Gezahlter Betrag (Unternehmenswährung),
|
Paid Amount (Company Currency),Gezahlter Betrag (Unternehmenswährung),
|
||||||
@ -4946,7 +4946,7 @@ Is Cumulative,Ist kumulativ,
|
|||||||
Coupon Code Based,Gutscheincode basiert,
|
Coupon Code Based,Gutscheincode basiert,
|
||||||
Discount on Other Item,Rabatt auf andere Artikel,
|
Discount on Other Item,Rabatt auf andere Artikel,
|
||||||
Apply Rule On Other,Regel auf andere anwenden,
|
Apply Rule On Other,Regel auf andere anwenden,
|
||||||
Party Information,Party Informationen,
|
Party Information,Informationen zur Partei,
|
||||||
Quantity and Amount,Menge und Menge,
|
Quantity and Amount,Menge und Menge,
|
||||||
Min Qty,Mindestmenge,
|
Min Qty,Mindestmenge,
|
||||||
Max Qty,Maximalmenge,
|
Max Qty,Maximalmenge,
|
||||||
@ -5048,7 +5048,7 @@ Group same items,Gruppe gleichen Artikel,
|
|||||||
Print Language,Drucksprache,
|
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",
|
"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,
|
Credit To,Gutschreiben auf,
|
||||||
Party Account Currency,Gruppenkonten-Währung,
|
Party Account Currency,Währung des Kontos der Partei,
|
||||||
Against Expense Account,Zu Aufwandskonto,
|
Against Expense Account,Zu Aufwandskonto,
|
||||||
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
|
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
|
||||||
Is Internal Supplier,Ist interner Lieferant,
|
Is Internal Supplier,Ist interner Lieferant,
|
||||||
@ -5650,7 +5650,7 @@ From Time ,Von-Zeit,
|
|||||||
Campaign Email Schedule,Kampagnen-E-Mail-Zeitplan,
|
Campaign Email Schedule,Kampagnen-E-Mail-Zeitplan,
|
||||||
Send After (days),Senden nach (Tage),
|
Send After (days),Senden nach (Tage),
|
||||||
Signed,Unterzeichnet,
|
Signed,Unterzeichnet,
|
||||||
Party User,Party Benutzer,
|
Party User,Benutzer der Partei,
|
||||||
Unsigned,Nicht unterzeichnet,
|
Unsigned,Nicht unterzeichnet,
|
||||||
Fulfilment Status,Erfüllungsstatus,
|
Fulfilment Status,Erfüllungsstatus,
|
||||||
N/A,nicht verfügbar,
|
N/A,nicht verfügbar,
|
||||||
@ -8598,7 +8598,7 @@ Territory-wise Sales,Gebietsbezogene Verkäufe,
|
|||||||
Total Stock Summary,Gesamt Stock Zusammenfassung,
|
Total Stock Summary,Gesamt Stock Zusammenfassung,
|
||||||
Trial Balance,Probebilanz,
|
Trial Balance,Probebilanz,
|
||||||
Trial Balance (Simple),Probebilanz (einfach),
|
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,
|
Unpaid Expense Claim,Ungezahlte Spesenabrechnung,
|
||||||
Warehouse wise Item Balance Age and Value,Lagerweise Item Balance Alter und Wert,
|
Warehouse wise Item Balance Age and Value,Lagerweise Item Balance Alter und Wert,
|
||||||
Work Order Stock Report,Arbeitsauftragsbericht,
|
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}",
|
"To overrule this, enable '{0}' in company {1}","Um dies zu überschreiben, aktivieren Sie '{0}' in Firma {1}",
|
||||||
Invalid condition expression,Ungültiger Bedingungsausdruck,
|
Invalid condition expression,Ungültiger Bedingungsausdruck,
|
||||||
Please Select a Company First,Bitte wählen Sie zuerst eine Firma aus,
|
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,
|
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,
|
Give number of days according to prior selection,Geben Sie die Anzahl der Tage gemäß vorheriger Auswahl an,
|
||||||
Email Details,E-Mail-Details,
|
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,
|
"Customer": 5,
|
||||||
"Delivery Note": 5,
|
"Delivery Note": 5,
|
||||||
"Employee": 3,
|
"Employee": 3,
|
||||||
"Instructor": 5,
|
|
||||||
"Issue": 5,
|
"Issue": 5,
|
||||||
"Item": 5,
|
"Item": 5,
|
||||||
"Journal Entry": 3,
|
"Journal Entry": 3,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user