Merge branch 'develop' into fix-miniscule-penalty

This commit is contained in:
Deepesh Garg 2022-05-14 21:36:27 +05:30 committed by GitHub
commit a8da657f81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2239 additions and 1007 deletions

View File

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",

View File

@ -12,10 +12,18 @@ Welcome to ERPNext issue tracker! Before creating an issue, please heed the foll
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
-->
**Is your feature request related to a problem? Please describe.**

2
.github/stale.yml vendored
View File

@ -25,7 +25,7 @@ pulls:
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- valid

View File

@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
return dimensions
def get_dimension_with_children(doctype, dimension):
def get_dimension_with_children(doctype, dimensions):
if isinstance(dimension, list):
dimension = dimension[0]
if isinstance(dimensions, str):
dimensions = [dimensions]
all_dimensions = []
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
for dimension in dimensions:
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
return all_dimensions

View File

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

View File

@ -346,6 +346,12 @@ class PaymentEntry(AccountsController):
)
)
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
frappe.throw(
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
title=_("Invalid Invoice"),
)
if ref_doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))

View File

@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
# block invoice after creating payment entry
# since `get_payment_entry` will not attach blocked invoice to payment
pi.block_invoice()
with self.assertRaises(frappe.ValidationError) as err:
pe.save()
self.assertTrue("is on hold" in str(err.exception).lower())
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
pos_opening_entry(frm) {
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
reset_values(frm);
frm.trigger("set_opening_amounts");
frm.trigger("get_pos_invoices");
frappe.run_serially([
() => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices")
]);
}
},
set_opening_amounts(frm) {
frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
.then(({ balance_details }) => {
balance_details.forEach(detail => {
frm.add_child("payment_reconciliation", {
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
},
get_pos_invoices(frm) {
frappe.call({
return frappe.call({
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),

View File

@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset
einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country
def test_einvoice_json(self):
@ -3139,6 +3140,39 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertTrue(si.items[0].serial_no)
def test_sales_invoice_with_disabled_account(self):
try:
account = frappe.get_doc("Account", "VAT 5% - _TC")
account.disabled = 1
account.save()
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.taxes = []
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TC",
"cost_center": "Main - _TC",
"description": "VAT @ 5.0",
"rate": 9,
},
)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue(
"Cannot create accounting entries against disabled accounts" in str(err.exception)
)
finally:
account.disabled = 0
account.save()
def test_gain_loss_with_advance_entry(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry

View File

@ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
from_date = to_date = posting_date
if not posting_date:
from_date = "1900-01-01"
to_date = "4000-01-01"
conditions = []
conditions = [
"""(from_date is null or from_date <= '{0}')
and (to_date is null or to_date >= '{1}')""".format(
from_date, to_date
if posting_date:
conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
)
]
else:
conditions.append("(from_date is null) and (to_date is null)")
conditions.append(
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))

View File

@ -31,6 +31,7 @@ def make_gl_entries(
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
@ -45,6 +46,26 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
Account = frappe.qb.DocType("Account")
disabled_accounts = (
frappe.qb.from_(Account)
.where(Account.name.isin(accounts) & Account.disabled == 1)
.select(Account.name, Account.disabled)
).run(as_dict=True)
if disabled_accounts:
account_list = "<br>"
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
frappe.throw(
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
title=_("Disabled Account Selected"),
)
def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql(
""" SELECT

View File

@ -507,7 +507,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
)
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""

View File

@ -275,7 +275,7 @@ def get_conditions(filters):
)
conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
conditions.append("{0} in %({0})s".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""
@ -435,7 +435,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle_map[group_by_value].entries.append(gle)
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
keylist = [
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount,
p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
p.customer, p.is_return {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}

View File

@ -237,7 +237,7 @@ def get_conditions(filters):
else:
conditions += (
common_condition
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions

View File

@ -405,7 +405,7 @@ def get_conditions(filters):
else:
conditions += (
common_condition
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions

View File

@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
additional_conditions += "and {0} in %({0})s".format(dimension.fieldname)
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
else:
additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname)
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})

View File

@ -637,6 +637,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
stock_entry.set_missing_values()
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
@ -724,7 +726,7 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
ste_doc.set_missing_values()
return ste_doc

View File

@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__('Supplier Quotation'),
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
function(){ frm.trigger("make_supplier_quotation") }, __("Create"));
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
@ -87,16 +87,24 @@ frappe.ui.form.on("Request for Quotation",{
},
make_suppplier_quotation: function(frm) {
make_supplier_quotation: function(frm) {
var doc = frm.doc;
var dialog = new frappe.ui.Dialog({
title: __("Create Supplier Quotation"),
fields: [
{ "fieldtype": "Select", "label": __("Supplier"),
{ "fieldtype": "Link",
"label": __("Supplier"),
"fieldname": "supplier",
"options": doc.suppliers.map(d => d.supplier),
"options": 'Supplier',
"reqd": 1,
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
get_query: () => {
return {
filters: [
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
]
}
}
}
],
primary_action_label: __("Create"),
primary_action: (args) => {

View File

@ -32,7 +32,9 @@
"terms",
"printing_settings",
"select_print_heading",
"letter_head"
"letter_head",
"more_info",
"opportunity"
],
"fields": [
{
@ -193,6 +195,23 @@
"options": "Letter Head",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text",
"print_hide": 1
},
{
"fieldname": "opportunity",
"fieldtype": "Link",
"label": "Opportunity",
"options": "Opportunity",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
@ -258,7 +277,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-11-24 17:47:49.909000",
"modified": "2022-04-06 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@ -327,4 +346,4 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

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

View File

@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition}
AND is_group = 0
AND company = %(company)s
AND disabled = %(disabled)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s
{mcond}
@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
dict(
account_types=filters.get("account_type"),
company=filters.get("company"),
disabled=filters.get("disabled", 0),
currency=company_currency,
txt="%{}%".format(txt),
offset=start,

View File

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

View File

@ -2,6 +2,6 @@ def get_data():
return {
"fieldname": "opportunity",
"transactions": [
{"items": ["Quotation", "Supplier Quotation"]},
{"items": ["Quotation", "Request for Quotation", "Supplier Quotation"]},
],
}

View File

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

View File

@ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase):
tax_rule_master = set_taxes(
quotation.party_name,
"Customer",
quotation.transaction_date,
None,
quotation.company,
customer_group=None,
supplier_group=None,

View File

@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
develop_version = "13.x.x-develop"
develop_version = "14.x.x-develop"
app_include_js = "erpnext.bundle.js"
app_include_css = "erpnext.bundle.css"

View File

@ -65,9 +65,10 @@ frappe.ui.form.on('Employee Advance', {
);
}
if (frm.doc.docstatus === 1 &&
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
if (
frm.doc.docstatus === 1
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount))
) {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
frm.add_custom_button(__("Return"), function() {
frm.trigger('make_return_entry');

View File

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

View File

@ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", {
});
}
}
// make new leaves allocated field read only if allocation is created via leave policy assignment
// and leave type is earned leave, since these leaves would be allocated via the scheduler
if (frm.doc.leave_policy_assignment) {
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
if (r && cint(r.is_earned_leave))
frm.set_df_property("new_leaves_allocated", "read_only", 1);
});
}
},
expire_allocation: function(frm) {

View File

@ -254,7 +254,18 @@ class LeaveAllocation(Document):
# Adding a day to include To Date in the difference
date_difference = date_diff(self.to_date, self.from_date) + 1
if date_difference < self.total_leaves_allocated:
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"):
frappe.msgprint(
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
indicator="orange",
alert=True,
)
else:
frappe.throw(
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
exc=OverAllocationError,
title=_("Over Allocation"),
)
def create_leave_ledger_entry(self, submit=True):
if self.unused_leaves:

View File

@ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase):
self.assertRaises(frappe.ValidationError, doc.save)
def test_validation_for_over_allocation(self):
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
leave_type.save()
doc = frappe.get_doc(
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"leave_type": leave_type.name,
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35,
"carry_forward": 1,
}
)
# allocated leave more than period
self.assertRaises(OverAllocationError, doc.save)
leave_type.allow_over_allocation = 1
leave_type.save()
# allows creating a leave allocation with more leave days than period days
doc = frappe.get_doc(
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": leave_type.name,
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35,
"carry_forward": 1,
}
).insert()
def test_validation_for_over_allocation_post_submission(self):
allocation = frappe.get_doc(
{

View File

@ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase):
i = 0
while i < 14:
allocate_earned_leaves(ignore_duplicates=True)
allocate_earned_leaves()
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
@ -753,7 +753,7 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
i = 0
while i < 6:
allocate_earned_leaves(ignore_duplicates=True)
allocate_earned_leaves()
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)

View File

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
@ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase):
class TestLeavePolicyAssignment(FrappeTestCase):
def setUp(self):
for doctype in [
"Leave Period",
@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy = create_leave_policy()
leave_policy.submit()
self.employee.date_of_joining = get_first_day(leave_period.from_date)
self.employee.save()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
@ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
)
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@ -242,20 +233,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(details.unused_leaves, 5)
self.assertEqual(details.total_leaves_allocated, 7)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import is_earned_leave_already_allocated
frappe.flags.current_date = get_last_day(getdate())
allocation = frappe.get_doc("Leave Allocation", details.name)
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
self.assertFalse(
is_earned_leave_already_allocated(
allocation, leave_policy.leave_policy_details[0].annual_allocation
)
)
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
leave_type = create_earned_leave_type("Test Earned Leave")
@ -288,19 +265,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_last_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
leave_period, leave_policy = setup_leave_period_and_policy(
@ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
)
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
@ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def tearDown(self):
frappe.db.rollback()
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
frappe.flags.current_date = None

View File

@ -19,6 +19,7 @@
"fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"allow_over_allocation",
"include_holiday",
"is_compensatory",
"carry_forward_section",
@ -211,15 +212,23 @@
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
"mandatory_depends_on": "eval:doc.is_ppl == 1"
},
{
"default": "0",
"description": "Allows allocating more leaves than the number of days in the allocation period.",
"fieldname": "allow_over_allocation",
"fieldtype": "Check",
"label": "Allow Over Allocation"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2021-10-02 11:59:40.503359",
"modified": "2022-05-09 05:01:38.957545",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@ -251,5 +260,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -4,21 +4,21 @@
frappe.query_reports["Employee Leave Balance"] = {
"filters": [
{
"fieldname":"from_date",
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_start_date")
},
{
"fieldname":"to_date",
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_end_date")
},
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@ -26,16 +26,29 @@ frappe.query_reports["Employee Leave Balance"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"department",
"fieldname": "department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department",
},
{
"fieldname":"employee",
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee",
},
{
"fieldname": "employee_status",
"label": __("Employee Status"),
"fieldtype": "Select",
"options": [
"",
{ "value": "Active", "label": __("Active") },
{ "value": "Inactive", "label": __("Inactive") },
{ "value": "Suspended", "label": __("Suspended") },
{ "value": "Left", "label": __("Left") },
],
"default": "Active",
}
],

View File

@ -168,9 +168,8 @@ def get_opening_balance(
def get_conditions(filters: Filters) -> Dict:
conditions = {
"status": "Active",
}
conditions = {}
if filters.get("employee"):
conditions["name"] = filters.get("employee")
@ -180,6 +179,9 @@ def get_conditions(filters: Filters) -> Dict:
if filters.get("department"):
conditions["department"] = filters.get("department")
if filters.get("employee_status"):
conditions["status"] = filters.get("employee_status")
return conditions

View File

@ -207,3 +207,40 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
allocation1.new_leaves_allocated - leave_application.total_leave_days
)
self.assertEqual(report[1][0].opening_balance, opening_balance)
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_employee_status_filter(self):
frappe.get_doc(test_records[0]).insert()
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
allocation = make_allocation_record(
employee=inactive_emp,
from_date=self.year_start,
to_date=self.year_end,
leaves=5,
)
# set employee as inactive
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
filters = frappe._dict(
{
"from_date": allocation.from_date,
"to_date": allocation.to_date,
"employee": inactive_emp,
"employee_status": "Active",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 0)
filters = frappe._dict(
{
"from_date": allocation.from_date,
"to_date": allocation.to_date,
"employee": inactive_emp,
"employee_status": "Inactive",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 1)

View File

@ -30,6 +30,19 @@ frappe.query_reports['Employee Leave Balance Summary'] = {
label: __('Department'),
fieldtype: 'Link',
options: 'Department',
},
{
fieldname: "employee_status",
label: __("Employee Status"),
fieldtype: "Select",
options: [
"",
{ "value": "Active", "label": __("Active") },
{ "value": "Inactive", "label": __("Inactive") },
{ "value": "Suspended", "label": __("Suspended") },
{ "value": "Left", "label": __("Left") },
],
default: "Active",
}
]
};

View File

@ -35,9 +35,10 @@ def get_columns(leave_types):
def get_conditions(filters):
conditions = {
"status": "Active",
"company": filters.company,
}
if filters.get("employee_status"):
conditions.update({"status": filters.get("employee_status")})
if filters.get("department"):
conditions.update({"department": filters.get("department")})
if filters.get("employee"):

View File

@ -36,7 +36,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
frappe.set_user("Administrator")
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
self.date = getdate()
@ -146,3 +145,37 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
]
self.assertEqual(report[1], expected_data)
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_employee_status_filter(self):
frappe.get_doc(test_records[0]).insert()
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
allocation = make_allocation_record(
employee=inactive_emp, from_date=self.year_start, to_date=self.year_end
)
# set employee as inactive
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
filters = frappe._dict(
{
"date": allocation.from_date,
"company": "_Test Company",
"employee": inactive_emp,
"employee_status": "Active",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 0)
filters = frappe._dict(
{
"date": allocation.from_date,
"company": "_Test Company",
"employee": inactive_emp,
"employee_status": "Inactive",
}
)
report = execute(filters)
self.assertEqual(len(report[1]), 1)

View File

@ -269,7 +269,7 @@ def generate_leave_encashment():
create_leave_encashment(leave_allocation=leave_allocation)
def allocate_earned_leaves(ignore_duplicates=False):
def allocate_earned_leaves():
"""Allocate earned leaves to Employees"""
e_leave_types = get_earned_leaves()
today = getdate()
@ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False):
if check_effective_date(
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
):
update_previous_leave_allocation(
allocation, annual_allocation, e_leave_type, ignore_duplicates
)
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(
allocation, annual_allocation, e_leave_type, ignore_duplicates=False
):
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
earned_leaves = get_monthly_earned_leave(
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
)
@ -326,20 +322,19 @@ def update_previous_leave_allocation(
if new_allocation != allocation.total_leaves_allocated:
today_date = today()
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
if e_leave_type.based_on_date_of_joining:
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
else:
text = _("allocated {0} leave(s) via scheduler on {1}").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
if e_leave_type.based_on_date_of_joining:
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
else:
text = _("allocated {0} leave(s) via scheduler on {1}").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
allocation.add_comment(comment_type="Info", text=text)
allocation.add_comment(comment_type="Info", text=text)
def get_monthly_earned_leave(annual_leaves, frequency, rounding):

View File

@ -764,8 +764,6 @@ def make_stock_entry(source_name, target_doc=None):
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()

View File

@ -313,7 +313,6 @@ erpnext.patches.v13_0.enable_uoms
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.healthcare_deprecation_warning
erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
@ -324,7 +323,6 @@ erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.update_tax_category_for_rcm
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.hospitality_deprecation_warning
@ -333,15 +331,17 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
erpnext.patches.v13_0.enable_ksa_vat_docs #1
erpnext.patches.v14_0.delete_education_doctypes
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v14_0.delete_education_doctypes
erpnext.patches.v14_0.delete_datev_doctypes
erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v14_0.update_leave_notification_template
@ -369,4 +369,5 @@ erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note

View File

@ -33,7 +33,7 @@ def execute():
"insert_after": insert_after_field,
}
create_custom_field(doctype, df, ignore_validate=False)
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
count += 1

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,8 @@ frappe.ui.form.on(cur_frm.doctype, {
query: "erpnext.controllers.queries.tax_account_query",
filters: {
"account_type": account_type,
"company": doc.company
"company": doc.company,
"disabled": 0
}
}
});

View File

@ -923,12 +923,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
currency() {
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
/* end manqala */
var me = this;
// The transaction date be either transaction_date (from orders) or posting_date (from invoices)
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
let me = this;
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
let company_currency = this.get_company_currency();
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
@ -942,7 +942,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
});
} else {
this.conversion_rate();
// company currency and doc currency is same
// this will prevent unnecessary conversion rate triggers
this.frm.set_value("conversion_rate", 1.0);
}
}
@ -1384,12 +1386,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this;
var args = this._get_args(item);
if (!(args.items && args.items.length)) {
if(calculate_taxes_and_totals) me.calculate_taxes_and_totals();
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}
// Target doc created from a mapped doc
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
// Calculate totals even though pricing rule is not applied.
// `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total.
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}

View File

@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
filters.splice(index, 0, {
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options(dimension["document_type"], txt);
},
});
}
});

View File

@ -10,6 +10,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
this.max_qty_field = opts.max_qty_field;
// scanner won't add a new row if this flag is set.
this.dont_allow_new_row = opts.dont_allow_new_row;
// scanner will ask user to type the quantity instead of incrementing by 1
this.prompt_qty = opts.prompt_qty;
this.items_table_name = opts.items_table_name || "items";
this.items_table = this.frm.doc[this.items_table_name];
@ -42,10 +48,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
.then((r) => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
frappe.show_alert({
message: __("Cannot find Item with this Barcode"),
indicator: "red",
});
this.show_alert(__("Cannot find Item with this Barcode"), "red");
this.clean_up();
return;
}
@ -56,22 +59,18 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
update_table(data) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
let row = null;
const {item_code, barcode, batch_no, serial_no} = data;
// Check if batch is scanned and table has batch no field
let batch_no_scan =
Boolean(batch_no) && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
if (batch_no_scan) {
row = this.get_batch_row_to_modify(batch_no);
} else {
// serial or barcode scan
row = this.get_row_to_modify_on_scan(item_code);
}
let row = this.get_row_to_modify_on_scan(item_code, batch_no);
if (!row) {
if (this.dont_allow_new_row) {
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
this.clean_up();
return;
}
// add new row if new item/batch is scanned
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
// trigger any row add triggers defined on child table.
@ -83,9 +82,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
return;
}
this.show_scan_message(row.idx, row.item_code);
this.set_selector_trigger_flag(row, data);
this.set_item(row, item_code);
this.set_item(row, item_code).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
});
this.set_serial_no(row, serial_no);
this.set_batch_no(row, batch_no);
this.set_barcode(row, barcode);
@ -106,9 +106,23 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
set_item(row, item_code) {
const item_data = { item_code: item_code };
item_data[this.qty_field] = (row[this.qty_field] || 0) + 1;
frappe.model.set_value(row.doctype, row.name, item_data);
return new Promise(resolve => {
const increment = (value = 1) => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
frappe.model.set_value(row.doctype, row.name, item_data);
};
if (this.prompt_qty) {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value);
resolve(value);
});
} else {
increment();
resolve();
}
});
}
set_serial_no(row, serial_no) {
@ -137,53 +151,42 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
show_scan_message(idx, exist = null) {
show_scan_message(idx, exist = null, qty = 1) {
// show new row or qty increase toast
if (exist) {
frappe.show_alert(
{
message: __("Row #{0}: Qty increased by 1", [idx]),
indicator: "green",
},
5
);
this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
} else {
frappe.show_alert(
{
message: __("Row #{0}: Item added", [idx]),
indicator: "green",
},
5
);
this.show_alert(__("Row #{0}: Item added", [idx]), "green")
}
}
is_duplicate_serial_no(row, serial_no) {
const is_duplicate = !!serial_no && !!row[this.serial_no_field]
&& row[this.serial_no_field].includes(serial_no);
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
if (is_duplicate) {
frappe.show_alert(
{
message: __("Serial No {0} is already added", [serial_no]),
indicator: "orange",
},
5
);
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
return is_duplicate;
}
get_batch_row_to_modify(batch_no) {
// get row if batch already exists in table
const existing_batch_row = this.items_table.find((d) => d.batch_no === batch_no);
return existing_batch_row || this.get_existing_blank_row();
}
get_row_to_modify_on_scan(item_code, batch_no) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
get_row_to_modify_on_scan(item_code) {
// get an existing item row to increment or blank row to modify
const existing_item_row = this.items_table.find((d) => d.item_code === item_code);
return existing_item_row || this.get_existing_blank_row();
// Check if batch is scanned and table has batch no field
let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);
const matching_row = (row) => {
const item_match = row.item_code == item_code;
const batch_match = row.batch_no == batch_no;
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
return item_match
&& (!is_batch_no_scan || batch_match)
&& (!check_max_qty || qty_in_limit)
}
return this.items_table.find(matching_row) || this.get_existing_blank_row();
}
get_existing_blank_row() {
@ -194,4 +197,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_barcode_field.set_value("");
refresh_field(this.items_table_name);
}
show_alert(msg, indicator, duration=3) {
frappe.show_alert({message: msg, indicator: indicator}, duration);
}
};

View File

@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => {
...data
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
callback: () => {
frappe.show_alert({
message: __('E-Way Bill Generated successfully'),
indicator: 'green'
}, 7);
frm.reload_doc();
d.hide();
},
error: () => {
frappe.show_alert({
message: __('E-Way Bill was not Generated'),
indicator: 'red'
}, 7);
d.hide();
}
});
},
primary_action_label: __('Submit')
@ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
const d = new frappe.ui.Dialog({
title: __('Cancel E-Way Bill'),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: {
doctype,
docname: name,
eway_bill: ewaybill,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
callback: () => {
frappe.show_alert({
message: __('E-Way Bill Cancelled successfully'),
indicator: 'green'
}, 7);
frm.reload_doc();
d.hide();
},
error: () => {
frappe.show_alert({
message: __('E-Way Bill was not Cancelled'),
indicator: 'red'
}, 7);
d.hide();
}
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
if (irn && !irn_cancelled) {
const action = () => {
const dialog = frappe.msgprint({
title: __('Update E-Way Bill Cancelled Status?'),
message: message,
indicator: 'orange',
title: __("Generate QRCode"),
message: __("Generate and attach QR Code using IRN?"),
primary_action: {
action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc() || dialog.hide()
callback: () => frm.reload_doc() || dialog.hide(),
error: () => dialog.hide()
});
}
},
primary_action_label: __('Yes')
});
dialog.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
add_custom_button(__("Generate QRCode"), action);
}
}
});
@ -167,85 +234,100 @@ erpnext.setup_einvoice_actions = (doctype) => {
const get_ewaybill_fields = (frm) => {
return [
{
'fieldname': 'transporter',
'label': 'Transporter',
'fieldtype': 'Link',
'options': 'Supplier',
'default': frm.doc.transporter
fieldname: "eway_part_a_section_break",
fieldtype: "Section Break",
label: "Part A",
},
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
'default': frm.doc.gst_transporter_id
fieldname: "transporter",
label: "Transporter",
fieldtype: "Link",
options: "Supplier",
default: frm.doc.transporter,
},
{
'fieldname': 'driver',
'label': 'Driver',
'fieldtype': 'Link',
'options': 'Driver',
'default': frm.doc.driver
fieldname: "transporter_name",
label: "Transporter Name",
fieldtype: "Data",
read_only: 1,
default: frm.doc.transporter_name,
depends_on: "transporter",
},
{
'fieldname': 'lr_no',
'label': 'Transport Receipt No',
'fieldtype': 'Data',
'default': frm.doc.lr_no
fieldname: "part_a_column_break",
fieldtype: "Column Break",
},
{
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
'default': frm.doc.vehicle_no
fieldname: "gst_transporter_id",
label: "GST Transporter ID",
fieldtype: "Data",
default: frm.doc.gst_transporter_id,
},
{
'fieldname': 'distance',
'label': 'Distance (in km)',
'fieldtype': 'Float',
'default': frm.doc.distance
fieldname: "distance",
label: "Distance (in km)",
fieldtype: "Float",
default: frm.doc.distance,
description: 'Set as zero to auto calculate distance using pin codes',
},
{
'fieldname': 'transporter_col_break',
'fieldtype': 'Column Break',
fieldname: "eway_part_b_section_break",
fieldtype: "Section Break",
label: "Part B",
},
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'read_only': 1,
'default': frm.doc.transporter_name,
'depends_on': 'transporter'
fieldname: "mode_of_transport",
label: "Mode of Transport",
fieldtype: "Select",
options: `\nRoad\nAir\nRail\nShip`,
default: frm.doc.mode_of_transport,
},
{
'fieldname': 'mode_of_transport',
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': `\nRoad\nAir\nRail\nShip`,
'default': frm.doc.mode_of_transport
fieldname: "gst_vehicle_type",
label: "GST Vehicle Type",
fieldtype: "Select",
options: `Regular\nOver Dimensional Cargo (ODC)`,
depends_on: 'eval:(doc.mode_of_transport === "Road")',
default: frm.doc.gst_vehicle_type,
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
'fieldtype': 'Data',
'fetch_from': 'driver.full_name',
'read_only': 1,
'default': frm.doc.driver_name,
'depends_on': 'driver'
fieldname: "vehicle_no",
label: "Vehicle No",
fieldtype: "Data",
default: frm.doc.vehicle_no,
},
{
'fieldname': 'lr_date',
'label': 'Transport Receipt Date',
'fieldtype': 'Date',
'default': frm.doc.lr_date
fieldname: "part_b_column_break",
fieldtype: "Column Break",
},
{
'fieldname': 'gst_vehicle_type',
'label': 'GST Vehicle Type',
'fieldtype': 'Select',
'options': `Regular\nOver Dimensional Cargo (ODC)`,
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.gst_vehicle_type
}
fieldname: "lr_date",
label: "Transport Receipt Date",
fieldtype: "Date",
default: frm.doc.lr_date,
},
{
fieldname: "lr_no",
label: "Transport Receipt No",
fieldtype: "Data",
default: frm.doc.lr_no,
},
{
fieldname: "driver",
label: "Driver",
fieldtype: "Link",
options: "Driver",
default: frm.doc.driver,
},
{
fieldname: "driver_name",
label: "Driver Name",
fieldtype: "Data",
fetch_from: "driver.full_name",
read_only: 1,
default: frm.doc.driver_name,
depends_on: "driver",
},
];
};

View File

@ -167,7 +167,12 @@ def get_doc_details(invoice):
title=_("Not Allowed"),
)
invoice_type = "CRN" if invoice.is_return else "INV"
if invoice.is_return:
invoice_type = "CRN"
elif invoice.is_debit_note:
invoice_type = "DBN"
else:
invoice_type = "INV"
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
@ -443,7 +448,7 @@ def get_eway_bill_details(invoice):
dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None,
distance=invoice.distance or 0,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
@ -792,8 +797,9 @@ class GSPConnector:
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
def set_invoice(self):
self.invoice = None
@ -857,8 +863,8 @@ class GSPConnector:
return res
def auto_refresh_token(self):
self.fetch_auth_token()
self.token_auto_refreshed = True
self.fetch_auth_token()
def log_request(self, url, headers, data, res):
headers.update({"password": self.credentials.password})
@ -998,6 +1004,37 @@ class GSPConnector:
return failed
def fetch_and_attach_qrcode_from_irn(self):
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
if qrcode:
qrcode_file = self.create_qr_code_file(qrcode)
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
else:
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
def get_qrcode_from_irn(self, irn):
import requests
headers = self.get_headers()
headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn})
try:
# using requests.get instead of make_request to avoid parsing the response
res = requests.get(self.get_qrcode_url, headers=headers)
self.log_request(self.get_qrcode_url, headers, None, None)
if res.status_code == 200:
return res.content
else:
raise RequestFailed(str(res.content, "utf-8"))
except RequestFailed as e:
self.raise_error(errors=str(e))
except Exception:
log_error()
self.raise_error()
def get_irn_details(self, irn):
headers = self.get_headers()
@ -1113,6 +1150,19 @@ class GSPConnector:
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
self.invoice.eway_bill_cancelled = 0
self.invoice.update(args)
if res.get("info"):
info = res.get("info")
# when we have more features (responses) in eway bill, we can add them using below forloop.
for msg in info:
if msg.get("InfCd") == "EWBPPD":
pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group())
frappe.msgprint(
_("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)),
title="Notification",
indicator="green",
alert=True,
)
self.invoice.distance = flt(pin_to_pin_distance)
self.invoice.flags.updater_reference = {
"doctype": self.invoice.doctype,
"docname": self.invoice.name,
@ -1135,7 +1185,6 @@ class GSPConnector:
headers = self.get_headers()
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"]
del headers["user_name"]
try:
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
if res.get("success"):
@ -1186,8 +1235,6 @@ class GSPConnector:
return errors
def raise_error(self, raise_exception=False, errors=None):
if errors is None:
errors = []
title = _("E Invoice Request Failed")
if errors:
frappe.throw(errors, title=title, as_list=1)
@ -1228,13 +1275,18 @@ class GSPConnector:
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
qr_image = io.BytesIO()
url = qrcreate(qrcode, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
qrcode_file = self.create_qr_code_file(qr_image.getvalue())
self.invoice.qrcode_image = qrcode_file.file_url
def create_qr_code_file(self, qr_image):
doctype = self.invoice.doctype
docname = self.invoice.name
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
_file = frappe.get_doc(
{
"doctype": "File",
@ -1243,12 +1295,12 @@ class GSPConnector:
"attached_to_name": docname,
"attached_to_field": "qrcode_image",
"is_private": 0,
"content": qr_image.getvalue(),
"content": qr_image,
}
)
_file.save()
frappe.db.commit()
self.invoice.qrcode_image = _file.file_url
return _file
def update_invoice(self):
self.invoice.flags.ignore_validate_update_after_submit = True
@ -1293,6 +1345,12 @@ def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector.cancel_irn(irn, reason, remark)
@frappe.whitelist()
def generate_qrcode(doctype, docname):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.fetch_and_attach_qrcode_from_irn()
@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
@ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
def cancel_eway_bill(doctype, docname):
# TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
frappe.db.set_value(doctype, docname, "ewaybill", "")
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
@frappe.whitelist()

View File

@ -32,7 +32,7 @@ def _execute(filters=None):
added_item = []
for d in item_list:
if (d.parent, d.item_code) not in added_item:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
total_tax = 0
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
@ -40,11 +40,9 @@ def _execute(filters=None):
row += [d.base_net_amount + total_tax]
row += [d.base_net_amount]
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)]
data.append(row)
added_item.append((d.parent, d.item_code))
if data:
@ -64,6 +62,7 @@ def get_columns():
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
{"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90},
{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
{
"fieldname": "taxable_amount",
@ -106,16 +105,25 @@ def get_items(filters):
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
`tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description,
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
from
`tabSales Invoice`,
`tabSales Invoice Item`,
`tabGST HSN Code`,
`tabSales Taxes and Charges`
where
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
group by
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
`tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code
"""
% (conditions, match_conditions),
filters,
@ -213,15 +221,16 @@ def get_merged_data(columns, data):
result = []
for row in data:
merged_hsn_dict.setdefault(row[0], {})
key = row[0] + "-" + str(row[4])
merged_hsn_dict.setdefault(key, {})
for i, d in enumerate(columns):
if d["fieldtype"] not in ("Int", "Float", "Currency"):
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
merged_hsn_dict[key][d["fieldname"]] = row[i]
else:
if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""):
merged_hsn_dict[row[0]][d["fieldname"]] += row[i]
if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""):
merged_hsn_dict[key][d["fieldname"]] += row[i]
else:
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
merged_hsn_dict[key][d["fieldname"]] = row[i]
for key, value in merged_hsn_dict.items():
result.append(value)
@ -240,7 +249,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp}
gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp}
gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)}
@ -271,7 +280,7 @@ def get_hsn_wise_json_data(filters, report_data):
"desc": hsn.get("description"),
"uqc": hsn.get("stock_uom").upper(),
"qty": hsn.get("stock_qty"),
"val": flt(hsn.get("total_amount"), 2),
"rt": flt(hsn.get("tax_rate"), 2),
"txval": flt(hsn.get("taxable_amount", 2)),
"iamt": 0.0,
"camt": 0.0,

View File

@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class {
frappe.dom.freeze();
this.frm = this.get_new_frm(this.frm);
this.frm.doc.items = [];
const res = await frappe.call({
return frappe.call({
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
args: {
'source_name': doc.name,
'target_doc': this.frm.doc
},
callback: (r) => {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
this.set_pos_profile_data().then(() => {
frappe.dom.unfreeze();
});
}
});
frappe.model.sync(res.message);
await this.set_pos_profile_data();
frappe.dom.unfreeze();
}
set_pos_profile_data() {

View File

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

View File

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

View File

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

View File

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

View File

@ -64,13 +64,18 @@ class Bin(Document):
se = frappe.qb.DocType("Stock Entry")
se_item = frappe.qb.DocType("Stock Entry Detail")
if frappe.db.field_exists("Stock Entry", "is_return"):
qty_field = (
Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
)
else:
qty_field = se_item.transfer_qty
materials_transferred = (
frappe.qb.from_(se)
.from_(se_item)
.from_(po)
.select(
Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty))
)
.select(Sum(qty_field))
.where(
(se.docstatus == 1)
& (se.purpose == "Send to Subcontractor")

View File

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

View File

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

View File

@ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
@ -180,9 +183,12 @@ def make_items():
if not frappe.db.exists("Item", item_code):
create_item(item_code)
create_stock_reconciliation(
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
)
try:
create_stock_reconciliation(
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
)
except EmptyStockReconciliationItemsError:
pass
if frappe.db.exists("Item", "Test FG A RW 1"):
doc = frappe.get_doc("Item", "Test FG A RW 1")

View File

@ -597,7 +597,7 @@ def make_stock_entry(source_name, target_doc=None):
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
target.run_method("calculate_rate_and_amount")
target.set_missing_values()
target.set_stock_entry_type()
target.set_job_card_data()

View File

@ -158,6 +158,19 @@ frappe.ui.form.on('Pick List', {
get_query_filters: get_query_filters
});
});
},
scan_barcode: (frm) => {
const opts = {
frm,
items_table_name: 'locations',
qty_field: 'picked_qty',
max_qty_field: 'qty',
dont_allow_new_row: true,
prompt_qty: frm.doc.prompt_qty,
serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field.
};
const barcode_scanner = new erpnext.utils.BarcodeScanner(opts);
barcode_scanner.process_scan();
}
});

View File

@ -17,6 +17,11 @@
"parent_warehouse",
"get_item_locations",
"section_break_6",
"scan_barcode",
"column_break_13",
"scan_mode",
"prompt_qty",
"section_break_15",
"locations",
"amended_from",
"print_settings_section",
@ -36,6 +41,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
@ -126,11 +132,38 @@
"fieldtype": "Check",
"label": "Group Same Items",
"print_hide": 1
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
"label": "Scan Barcode",
"options": "Barcode"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If checked, picked qty won't automatically be fulfilled on submit of pick list.",
"fieldname": "scan_mode",
"fieldtype": "Check",
"label": "Scan Mode"
},
{
"default": "0",
"fieldname": "prompt_qty",
"fieldtype": "Check",
"label": "Prompt Qty"
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-04-21 07:56:40.646473",
"modified": "2022-05-11 09:09:53.029312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@ -41,8 +41,15 @@ class PickList(Document):
def before_submit(self):
update_sales_orders = set()
for item in self.locations:
# if the user has not entered any picked qty, set it to stock_qty, before submit
if item.picked_qty == 0:
if self.scan_mode and item.picked_qty < item.stock_qty:
frappe.throw(
_(
"Row {0} picked quantity is less than the required quantity, additional {1} {2} required."
).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
title=_("Pick List Incomplete"),
)
elif not self.scan_mode and item.picked_qty == 0:
# if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty
if item.sales_order_item:
@ -672,8 +679,7 @@ def create_stock_entry(pick_list):
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
stock_entry.set_actual_qty()
stock_entry.calculate_rate_and_amount()
stock_entry.set_missing_values()
return stock_entry.as_dict()

View File

@ -202,4 +202,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@ -1036,6 +1036,7 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.stock_entry_type = "Material Transfer"
target.purpose = "Material Transfer"
target.set_missing_values()
doclist = get_mapped_doc(
"Purchase Receipt",

View File

@ -470,7 +470,9 @@ frappe.ui.form.on('Stock Entry', {
},
callback: function(r) {
if (!r.exc) {
$.extend(child, r.message);
["actual_qty", "basic_rate"].forEach((field) => {
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
});
frm.events.calculate_basic_amount(frm, child);
}
}
@ -1057,8 +1059,8 @@ function attach_bom_items(bom_no) {
function check_should_not_attach_bom_items(bom_no) {
return (
bom_no === undefined ||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
bom_no === undefined ||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
);
}

View File

@ -2197,6 +2197,12 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
@ -2246,6 +2252,7 @@ def move_sample_to_retention_warehouse(company, items):
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.set_stock_entry_type()
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
target_doc.t_warehouse = ""

View File

@ -132,6 +132,7 @@ def make_stock_entry(**args):
)
s.set_stock_entry_type()
if not args.do_not_save:
s.insert()
if not args.do_not_submit:

View File

@ -652,6 +652,104 @@ class TestStockEntry(FrappeTestCase):
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
def test_serial_batch_item_stock_entry(self):
"""
Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
2) Cancel same Stock Entry
Expected Result: 1) Batch is created with Reference in Serial No
2) Batch is deleted and Serial No is Inactive
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "B-BATCH-.##"
item.serial_no_series = "S-.####"
item.save()
else:
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
se = make_stock_entry(
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
)
batch_no = se.items[0].batch_no
serial_no = get_serial_nos(se.items[0].serial_no)[0]
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
self.assertEqual(batch_in_serial_no, batch_no)
self.assertEqual(batch_qty, 1)
se.cancel()
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
self.assertEqual(batch_in_serial_no, None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
def test_serial_batch_item_qty_deduction(self):
"""
Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
Expected: 1) Cancelling first Stock Entry (origin transaction of created batch)
should throw a LinkExistsError
2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
and in that transaction only, Inactive.
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "B-BATCH-.##"
item.serial_no_series = "S-.####"
item.save()
else:
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
se1 = make_stock_entry(
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
)
batch_no = se1.items[0].batch_no
serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
# Check Source (Origin) Document of Batch
self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
se2 = make_stock_entry(
item_code=item.item_code,
target="_Test Warehouse - _TC",
qty=1,
basic_rate=100,
batch_no=batch_no,
)
serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
self.assertEqual(batch_qty, 2)
se2.cancel()
# Check decrease in Batch Qty
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
self.assertEqual(batch_qty, 1)
# Check if Serial No from Stock Entry 1 is intact
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
# Check if Serial No from Stock Entry 2 is Unlinked and Inactive
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
def test_warehouse_company_validation(self):
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
frappe.get_doc("User", "test2@example.com").add_roles(
@ -1326,6 +1424,25 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, se.save)
def test_mapped_stock_entry(self):
"Check if rate and stock details are populated in mapped SE given warehouse."
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item_code = "_TestMappedItem"
create_item(item_code, is_stock_item=True)
pr = make_purchase_receipt(
item_code=item_code, qty=2, rate=100, company="_Test Company", warehouse="Stores - _TC"
)
mapped_se = make_stock_entry(pr.name)
self.assertEqual(mapped_se.items[0].s_warehouse, "Stores - _TC")
self.assertEqual(mapped_se.items[0].actual_qty, 2)
self.assertEqual(mapped_se.items[0].basic_rate, 100)
self.assertEqual(mapped_se.items[0].basic_amount, 200)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

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

View File

@ -170,6 +170,7 @@
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_22",
"fieldtype": "Section Break"
},
@ -182,7 +183,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-27 08:57:47.161959",
"modified": "2022-05-11 09:10:26.327652",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",

View File

@ -62,6 +62,7 @@ class StockReconciliation(StockController):
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
@ -456,7 +457,7 @@ class StockReconciliation(StockController):
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
d.total_amount = d.actual_qty * d.valuation_rate
d.total_amount = flt(d.actual_qty) * d.valuation_rate
merge_similar_entries[key] = d
elif d.serial_no:
data = merge_similar_entries[key]

View File

@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
def tearDown(self):
frappe.local.future_sle = {}
frappe.flags.pop("dont_execute_stock_reposts", None)
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase):
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1
)
sr.save()
sr.submit()
@ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
def test_stock_reco_for_serial_and_batch_item(self):
item = create_item("_TestBatchSerialItemReco")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "TBS-BATCH-.##"
item.serial_no_series = "TBS-.####"
item.save()
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100)
batch_no = sr.items[0].batch_no
serial_nos = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos), 1)
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
sr.cancel()
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
"""
Behaviour: 1) Create Stock Reconciliation, which will be the origin document
of a new batch having a serial no
2) Create a Stock Entry that adds a serial no to the same batch following this
Stock Reconciliation
3) Cancel Stock Entry
Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item = create_item("_TestBatchSerialItemDependentReco")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "TBSD-BATCH-.##"
item.serial_no_series = "TBSD-.####"
item.save()
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
stock_reco = create_stock_reconciliation(
item_code=item.item_code, warehouse=warehouse, qty=1, rate=100
)
batch_no = stock_reco.items[0].batch_no
reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
stock_entry = make_stock_entry(
item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no
)
serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
# Check Batch qty after 2 transactions
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
self.assertEqual(batch_qty, 2)
# Cancel latest stock document
stock_entry.cancel()
# Check Batch qty after cancellation
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
self.assertEqual(batch_qty, 1)
# Check if Serial No from Stock Reconcilation is intact
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no)
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active")
# Check if Serial No from Stock Entry is Unlinked and Inactive
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
stock_reco.cancel()
def test_customer_provided_items(self):
item_code = "Stock-Reco-customer-Item-100"
create_item(
@ -306,6 +385,7 @@ class TestStockReconciliation(FrappeTestCase):
-------------------------------------------
Var | Doc | Qty | Balance
-------------------------------------------
PR5 | PR | 10 | 10 (posting date: today-4) [backdated]
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
PR1 | PR | 10 | 18 (posting date: today-3)
PR2 | PR | 1 | 19 (posting date: today-2)
@ -315,6 +395,14 @@ class TestStockReconciliation(FrappeTestCase):
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
frappe.flags.dont_execute_stock_reposts = True
def assertBalance(doc, qty_after_transaction):
sle_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction"
)
self.assertEqual(sle_balance, qty_after_transaction)
pr1 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
)
@ -324,62 +412,37 @@ class TestStockReconciliation(FrappeTestCase):
pr3 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
)
pr1_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
)
pr3_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
)
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr3_balance, 12)
assertBalance(pr1, 10)
assertBalance(pr3, 12)
# post backdated stock reco in between
sr4 = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
)
pr3_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
)
self.assertEqual(pr3_balance, 7)
assertBalance(pr3, 7)
# post backdated stock reco at the start
sr5 = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
)
pr1_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
assertBalance(pr1, 18)
assertBalance(pr2, 19)
assertBalance(sr4, 6) # check if future stock reco is unaffected
# Make a backdated receipt and check only entries till first SR are affected
pr5 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5)
)
pr2_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
)
sr4_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
)
self.assertEqual(pr1_balance, 18)
self.assertEqual(pr2_balance, 19)
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
assertBalance(pr5, 10)
# check if future stock reco is unaffected
assertBalance(sr4, 6)
assertBalance(sr5, 8)
# cancel backdated stock reco and check future impact
sr5.cancel()
pr1_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
)
pr2_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
)
sr4_balance = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
)
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr2_balance, 11)
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
# teardown
sr4.cancel()
pr3.cancel()
pr2.cancel()
pr1.cancel()
assertBalance(pr1, 10)
assertBalance(pr2, 11)
assertBalance(sr4, 6) # check if future stock reco is unaffected
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_backdated_stock_reco_future_negative_stock(self):
@ -485,7 +548,6 @@ class TestStockReconciliation(FrappeTestCase):
# repost will make this test useless, qty should update in realtime without reposts
frappe.flags.dont_execute_stock_reposts = True
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
@ -684,11 +746,13 @@ def create_stock_reconciliation(**args):
},
)
try:
if not args.do_not_submit:
sr.submit()
except EmptyStockReconciliationItemsError:
pass
if not args.do_not_save:
sr.insert()
try:
if not args.do_not_submit:
sr.submit()
except EmptyStockReconciliationItemsError:
pass
return sr

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import datetime
from typing import List
import frappe
from frappe import _, scrub
@ -148,18 +149,26 @@ def get_periodic_data(entry, filters):
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
"""
expected_ranges = get_period_date_ranges(filters)
expected_periods = []
for _start_date, end_date in expected_ranges:
expected_periods.append(get_period(end_date, filters))
periodic_data = {}
for d in entry:
period = get_period(d.posting_date, filters)
bal_qty = 0
fill_intermediate_periods(periodic_data, d.item_code, period, expected_periods)
# if period against item does not exist yet, instantiate it
# insert existing balance dict against period, and add/subtract to it
if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period):
previous_balance = periodic_data[d.item_code]["balance"].copy()
periodic_data[d.item_code][period] = previous_balance
if d.voucher_type == "Stock Reconciliation":
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get(
d.warehouse
):
@ -186,6 +195,36 @@ def get_periodic_data(entry, filters):
return periodic_data
def fill_intermediate_periods(
periodic_data, item_code: str, current_period: str, all_periods: List[str]
) -> None:
"""There might be intermediate periods where no stock ledger entry exists, copy previous previous data.
Previous data is ONLY copied if period falls in report range and before period being processed currently.
args:
current_period: process till this period (exclusive)
all_periods: all periods expected in report via filters
periodic_data: report's periodic data
item_code: item_code being processed
"""
previous_period_data = None
for period in all_periods:
if period == current_period:
return
if (
periodic_data.get(item_code)
and not periodic_data.get(item_code).get(period)
and previous_period_data
):
# This period should exist since it's in report range, assign previous period data
periodic_data[item_code][period] = previous_period_data.copy()
previous_period_data = periodic_data.get(item_code, {}).get(period)
def get_data(filters):
data = []
items = get_items(filters)
@ -194,6 +233,8 @@ def get_data(filters):
periodic_data = get_periodic_data(sle, filters)
ranges = get_period_date_ranges(filters)
today = getdate()
for dummy, item_data in item_details.items():
row = {
"name": item_data.name,
@ -202,14 +243,15 @@ def get_data(filters):
"uom": item_data.stock_uom,
"brand": item_data.brand,
}
total = 0
for dummy, end_date in ranges:
previous_period_value = 0.0
for start_date, end_date in ranges:
period = get_period(end_date, filters)
period_data = periodic_data.get(item_data.name, {}).get(period)
amount = sum(period_data.values()) if period_data else 0
row[scrub(period)] = amount
total += amount
row["total"] = total
if period_data:
row[scrub(period)] = previous_period_value = sum(period_data.values())
else:
row[scrub(period)] = previous_period_value if today >= start_date else None
data.append(row)
return data

View File

@ -1,13 +1,59 @@
import datetime
import frappe
from frappe import _dict
from frappe.tests.utils import FrappeTestCase
from frappe.utils.data import add_to_date, get_datetime, getdate, nowdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_analytics.stock_analytics import execute, get_period_date_ranges
def stock_analytics(filters):
col, data, *_ = execute(filters)
return col, data
class TestStockAnalyticsReport(FrappeTestCase):
def setUp(self) -> None:
self.item = make_item().name
self.warehouse = "_Test Warehouse - _TC"
def assert_single_item_report(self, movement, expected_buckets):
self.generate_stock(movement)
filters = _dict(
range="Monthly",
from_date=movement[0][1].replace(day=1),
to_date=movement[-1][1].replace(day=28),
value_quantity="Quantity",
company="_Test Company",
item_code=self.item,
)
cols, data = stock_analytics(filters)
self.assertEqual(len(data), 1)
row = frappe._dict(data[0])
self.assertEqual(row.name, self.item)
self.compare_analytics_row(row, cols, expected_buckets)
def generate_stock(self, movement):
for qty, posting_date in movement:
args = {"item": self.item, "qty": abs(qty), "posting_date": posting_date}
args["to_warehouse" if qty > 0 else "from_warehouse"] = self.warehouse
make_stock_entry(**args)
def compare_analytics_row(self, report_row, columns, expected_buckets):
# last (N) cols will be monthly data
no_of_buckets = len(expected_buckets)
month_cols = [col["fieldname"] for col in columns[-no_of_buckets:]]
actual_buckets = [report_row.get(col) for col in month_cols]
self.assertEqual(actual_buckets, expected_buckets)
def test_get_period_date_ranges(self):
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
@ -33,3 +79,38 @@ class TestStockAnalyticsReport(FrappeTestCase):
]
self.assertEqual(ranges, expected_ranges)
def test_basic_report_functionality(self):
"""Stock analytics report generates balance "as of" periods based on
user defined ranges. Check that this behaviour is correct."""
# create stock movement in 3 months at 15th of month
today = getdate()
movement = [
(10, add_to_date(today, months=0).replace(day=15)),
(-5, add_to_date(today, months=1).replace(day=15)),
(10, add_to_date(today, months=2).replace(day=15)),
]
self.assert_single_item_report(movement, [10, 5, 15])
def test_empty_month_in_between(self):
today = getdate()
movement = [
(100, add_to_date(today, months=0).replace(day=15)),
(-50, add_to_date(today, months=1).replace(day=15)),
# Skip a month
(20, add_to_date(today, months=3).replace(day=15)),
]
self.assert_single_item_report(movement, [100, 50, 50, 70])
def test_multi_month_missings(self):
today = getdate()
movement = [
(100, add_to_date(today, months=0).replace(day=15)),
(-50, add_to_date(today, months=1).replace(day=15)),
# Skip a month
(20, add_to_date(today, months=3).replace(day=15)),
# Skip another month
(-10, add_to_date(today, months=5).replace(day=15)),
]
self.assert_single_item_report(movement, [100, 50, 50, 70, 70, 60])

View File

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

View File

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

View File

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

View File

@ -40,3 +40,8 @@ class TestInit(unittest.TestCase):
enc_name == expected_names[i],
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]),
)
def test_translation_files(self):
from frappe.tests.test_translate import verify_translation_files
verify_translation_files("erpnext")

View File

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

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

File diff suppressed because it is too large Load Diff