Merge branch 'develop' into fix-miniscule-penalty
This commit is contained in:
commit
a8da657f81
@ -5,7 +5,7 @@
|
|||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 9,
|
"ecmaVersion": 11,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
|
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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
|
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 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
|
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||||
the original discussion.
|
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.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@ -25,7 +25,7 @@ pulls:
|
|||||||
ready. Thank you for contributing.
|
ready. Thank you for contributing.
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
daysUntilStale: 60
|
daysUntilStale: 90
|
||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- valid
|
- valid
|
||||||
|
@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
|
|||||||
return dimensions
|
return dimensions
|
||||||
|
|
||||||
|
|
||||||
def get_dimension_with_children(doctype, dimension):
|
def get_dimension_with_children(doctype, dimensions):
|
||||||
|
|
||||||
if isinstance(dimension, list):
|
if isinstance(dimensions, str):
|
||||||
dimension = dimension[0]
|
dimensions = [dimensions]
|
||||||
|
|
||||||
all_dimensions = []
|
all_dimensions = []
|
||||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
|
||||||
children = frappe.get_all(
|
for dimension in dimensions:
|
||||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||||
)
|
children = frappe.get_all(
|
||||||
all_dimensions += [c.name for c in children]
|
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||||
|
)
|
||||||
|
all_dimensions += [c.name for c in children]
|
||||||
|
|
||||||
return all_dimensions
|
return all_dimensions
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
"bank_account_no",
|
"bank_account_no",
|
||||||
"address_and_contact",
|
"address_and_contact",
|
||||||
"address_html",
|
"address_html",
|
||||||
"website",
|
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"contact_html",
|
"contact_html",
|
||||||
"integration_details_section",
|
"integration_details_section",
|
||||||
@ -156,11 +155,6 @@
|
|||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Address HTML"
|
"label": "Address HTML"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "website",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Website"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_13",
|
"fieldname": "column_break_13",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@ -208,7 +202,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-23 16:48:06.303658",
|
"modified": "2022-05-04 15:49:42.620630",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
@ -243,5 +237,6 @@
|
|||||||
"search_fields": "bank,account",
|
"search_fields": "bank,account",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -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:
|
if ref_doc.docstatus != 1:
|
||||||
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
||||||
|
|
||||||
|
@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
|
|||||||
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
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):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
pos_opening_entry(frm) {
|
pos_opening_entry(frm) {
|
||||||
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
|
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
|
||||||
reset_values(frm);
|
reset_values(frm);
|
||||||
frm.trigger("set_opening_amounts");
|
frappe.run_serially([
|
||||||
frm.trigger("get_pos_invoices");
|
() => frm.trigger("set_opening_amounts"),
|
||||||
|
() => frm.trigger("get_pos_invoices")
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set_opening_amounts(frm) {
|
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 }) => {
|
.then(({ balance_details }) => {
|
||||||
balance_details.forEach(detail => {
|
balance_details.forEach(detail => {
|
||||||
frm.add_child("payment_reconciliation", {
|
frm.add_child("payment_reconciliation", {
|
||||||
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_pos_invoices(frm) {
|
get_pos_invoices(frm) {
|
||||||
frappe.call({
|
return frappe.call({
|
||||||
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
|
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
|
||||||
args: {
|
args: {
|
||||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||||
|
@ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
# reset
|
# reset
|
||||||
einvoice_settings = frappe.get_doc("E Invoice Settings")
|
einvoice_settings = frappe.get_doc("E Invoice Settings")
|
||||||
einvoice_settings.enable = 0
|
einvoice_settings.enable = 0
|
||||||
|
einvoice_settings.save()
|
||||||
frappe.flags.country = country
|
frappe.flags.country = country
|
||||||
|
|
||||||
def test_einvoice_json(self):
|
def test_einvoice_json(self):
|
||||||
@ -3139,6 +3140,39 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.reload()
|
si.reload()
|
||||||
self.assertTrue(si.items[0].serial_no)
|
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):
|
def test_gain_loss_with_advance_entry(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
|
|
||||||
|
@ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None):
|
|||||||
def get_tax_template(posting_date, args):
|
def get_tax_template(posting_date, args):
|
||||||
"""Get matching tax rule"""
|
"""Get matching tax rule"""
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
from_date = to_date = posting_date
|
conditions = []
|
||||||
if not posting_date:
|
|
||||||
from_date = "1900-01-01"
|
|
||||||
to_date = "4000-01-01"
|
|
||||||
|
|
||||||
conditions = [
|
if posting_date:
|
||||||
"""(from_date is null or from_date <= '{0}')
|
conditions.append(
|
||||||
and (to_date is null or to_date >= '{1}')""".format(
|
f"""(from_date is null or from_date <= '{posting_date}')
|
||||||
from_date, to_date
|
and (to_date is null or to_date >= '{posting_date}')"""
|
||||||
)
|
)
|
||||||
]
|
else:
|
||||||
|
conditions.append("(from_date is null) and (to_date is null)")
|
||||||
|
|
||||||
conditions.append(
|
conditions.append(
|
||||||
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
||||||
|
@ -31,6 +31,7 @@ def make_gl_entries(
|
|||||||
if gl_map:
|
if gl_map:
|
||||||
if not cancel:
|
if not cancel:
|
||||||
validate_accounting_period(gl_map)
|
validate_accounting_period(gl_map)
|
||||||
|
validate_disabled_accounts(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
if gl_map and len(gl_map) > 1:
|
if gl_map and len(gl_map) > 1:
|
||||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
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)
|
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):
|
def validate_accounting_period(gl_map):
|
||||||
accounting_periods = frappe.db.sql(
|
accounting_periods = frappe.db.sql(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
|
@ -507,7 +507,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
|||||||
)
|
)
|
||||||
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
else:
|
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 ""
|
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
||||||
|
|
||||||
|
@ -275,7 +275,7 @@ def get_conditions(filters):
|
|||||||
)
|
)
|
||||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
else:
|
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 ""
|
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)
|
gle_map[group_by_value].entries.append(gle)
|
||||||
|
|
||||||
elif group_by_voucher_consolidated:
|
elif group_by_voucher_consolidated:
|
||||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
keylist = [
|
||||||
|
gle.get("voucher_type"),
|
||||||
|
gle.get("voucher_no"),
|
||||||
|
gle.get("account"),
|
||||||
|
gle.get("party_type"),
|
||||||
|
gle.get("party"),
|
||||||
|
]
|
||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
|
@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
||||||
p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount,
|
p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
|
||||||
p.customer, p.is_return {select_mop_field}
|
p.customer, p.is_return {select_mop_field}
|
||||||
FROM
|
FROM
|
||||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||||
|
@ -237,7 +237,7 @@ def get_conditions(filters):
|
|||||||
else:
|
else:
|
||||||
conditions += (
|
conditions += (
|
||||||
common_condition
|
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
|
return conditions
|
||||||
|
@ -405,7 +405,7 @@ def get_conditions(filters):
|
|||||||
else:
|
else:
|
||||||
conditions += (
|
conditions += (
|
||||||
common_condition
|
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
|
return conditions
|
||||||
|
@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
|
|||||||
filters[dimension.fieldname] = get_dimension_with_children(
|
filters[dimension.fieldname] = get_dimension_with_children(
|
||||||
dimension.document_type, filters.get(dimension.fieldname)
|
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:
|
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)})
|
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
|
||||||
|
|
||||||
|
@ -637,6 +637,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||||
|
|
||||||
|
stock_entry.set_missing_values()
|
||||||
return stock_entry.as_dict()
|
return stock_entry.as_dict()
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("No Items selected for transfer"))
|
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)
|
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||||
|
|
||||||
ste_doc.set_stock_entry_type()
|
ste_doc.set_stock_entry_type()
|
||||||
ste_doc.calculate_rate_and_amount()
|
ste_doc.set_missing_values()
|
||||||
|
|
||||||
return ste_doc
|
return ste_doc
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
|
|
||||||
frm.add_custom_button(__('Supplier Quotation'),
|
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() {
|
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 doc = frm.doc;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("Create Supplier Quotation"),
|
title: __("Create Supplier Quotation"),
|
||||||
fields: [
|
fields: [
|
||||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
{ "fieldtype": "Link",
|
||||||
|
"label": __("Supplier"),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"options": doc.suppliers.map(d => d.supplier),
|
"options": 'Supplier',
|
||||||
"reqd": 1,
|
"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_label: __("Create"),
|
||||||
primary_action: (args) => {
|
primary_action: (args) => {
|
||||||
|
@ -32,7 +32,9 @@
|
|||||||
"terms",
|
"terms",
|
||||||
"printing_settings",
|
"printing_settings",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"letter_head"
|
"letter_head",
|
||||||
|
"more_info",
|
||||||
|
"opportunity"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -193,6 +195,23 @@
|
|||||||
"options": "Letter Head",
|
"options": "Letter Head",
|
||||||
"print_hide": 1
|
"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",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@ -258,7 +277,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-11-24 17:47:49.909000",
|
"modified": "2022-04-06 17:47:49.909000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
@ -327,4 +346,4 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC"
|
||||||
}
|
}
|
||||||
|
@ -2451,11 +2451,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_quantity(child_item, d):
|
def validate_quantity(child_item, new_data):
|
||||||
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
|
if not flt(new_data.get("qty")):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
|
||||||
|
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||||
|
),
|
||||||
|
title=_("Invalid Qty"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
||||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
||||||
|
|
||||||
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
|
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
|
||||||
|
child_item.received_qty
|
||||||
|
):
|
||||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||||
|
|
||||||
data = json.loads(trans_items)
|
data = json.loads(trans_items)
|
||||||
|
@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{account_type_condition}
|
{account_type_condition}
|
||||||
AND is_group = 0
|
AND is_group = 0
|
||||||
AND company = %(company)s
|
AND company = %(company)s
|
||||||
|
AND disabled = %(disabled)s
|
||||||
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||||
AND `{searchfield}` LIKE %(txt)s
|
AND `{searchfield}` LIKE %(txt)s
|
||||||
{mcond}
|
{mcond}
|
||||||
@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
dict(
|
dict(
|
||||||
account_types=filters.get("account_type"),
|
account_types=filters.get("account_type"),
|
||||||
company=filters.get("company"),
|
company=filters.get("company"),
|
||||||
|
disabled=filters.get("disabled", 0),
|
||||||
currency=company_currency,
|
currency=company_currency,
|
||||||
txt="%{}%".format(txt),
|
txt="%{}%".format(txt),
|
||||||
offset=start,
|
offset=start,
|
||||||
|
@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.utils import cint, cstr, flt, get_fullname
|
from frappe.utils import cint, flt, get_fullname
|
||||||
|
|
||||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
@ -215,20 +215,20 @@ class Opportunity(TransactionBase):
|
|||||||
|
|
||||||
if self.party_name and self.opportunity_from == "Customer":
|
if self.party_name and self.opportunity_from == "Customer":
|
||||||
if self.contact_person:
|
if self.contact_person:
|
||||||
opts.description = "Contact " + cstr(self.contact_person)
|
opts.description = f"Contact {self.contact_person}"
|
||||||
else:
|
else:
|
||||||
opts.description = "Contact customer " + cstr(self.party_name)
|
opts.description = f"Contact customer {self.party_name}"
|
||||||
elif self.party_name and self.opportunity_from == "Lead":
|
elif self.party_name and self.opportunity_from == "Lead":
|
||||||
if self.contact_display:
|
if self.contact_display:
|
||||||
opts.description = "Contact " + cstr(self.contact_display)
|
opts.description = f"Contact {self.contact_display}"
|
||||||
else:
|
else:
|
||||||
opts.description = "Contact lead " + cstr(self.party_name)
|
opts.description = f"Contact lead {self.party_name}"
|
||||||
|
|
||||||
opts.subject = opts.description
|
opts.subject = opts.description
|
||||||
opts.description += ". By : " + cstr(self.contact_by)
|
opts.description += f". By : {self.contact_by}"
|
||||||
|
|
||||||
if self.to_discuss:
|
if self.to_discuss:
|
||||||
opts.description += " To Discuss : " + cstr(self.to_discuss)
|
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||||
|
|
||||||
super(Opportunity, self).add_calendar_event(opts, force)
|
super(Opportunity, self).add_calendar_event(opts, force)
|
||||||
|
|
||||||
|
@ -2,6 +2,6 @@ def get_data():
|
|||||||
return {
|
return {
|
||||||
"fieldname": "opportunity",
|
"fieldname": "opportunity",
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"items": ["Quotation", "Supplier Quotation"]},
|
{"items": ["Quotation", "Request for Quotation", "Supplier Quotation"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import now_datetime, random_string, today
|
from frappe.utils import add_days, now_datetime, random_string, today
|
||||||
|
|
||||||
from erpnext.crm.doctype.lead.lead import make_customer
|
from erpnext.crm.doctype.lead.lead import make_customer
|
||||||
from erpnext.crm.doctype.lead.test_lead import make_lead
|
from erpnext.crm.doctype.lead.test_lead import make_lead
|
||||||
@ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase):
|
|||||||
self.assertEqual(quotation_comment_count, 4)
|
self.assertEqual(quotation_comment_count, 4)
|
||||||
self.assertEqual(quotation_communication_count, 4)
|
self.assertEqual(quotation_communication_count, 4)
|
||||||
|
|
||||||
|
def test_render_template_for_to_discuss(self):
|
||||||
|
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||||
|
doc.contact_by = "test@example.com"
|
||||||
|
doc.contact_date = add_days(today(), days=2)
|
||||||
|
doc.to_discuss = "{{ doc.name }} test data"
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
event = frappe.get_all(
|
||||||
|
"Event Participants",
|
||||||
|
fields=["parent"],
|
||||||
|
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||||
|
self.assertTrue(doc.name in event_description)
|
||||||
|
|
||||||
|
|
||||||
def make_opportunity_from_lead():
|
def make_opportunity_from_lead():
|
||||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||||
|
@ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
tax_rule_master = set_taxes(
|
tax_rule_master = set_taxes(
|
||||||
quotation.party_name,
|
quotation.party_name,
|
||||||
"Customer",
|
"Customer",
|
||||||
quotation.transaction_date,
|
None,
|
||||||
quotation.company,
|
quotation.company,
|
||||||
customer_group=None,
|
customer_group=None,
|
||||||
supplier_group=None,
|
supplier_group=None,
|
||||||
|
@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext"
|
|||||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||||
|
|
||||||
|
|
||||||
develop_version = "13.x.x-develop"
|
develop_version = "14.x.x-develop"
|
||||||
|
|
||||||
app_include_js = "erpnext.bundle.js"
|
app_include_js = "erpnext.bundle.js"
|
||||||
app_include_css = "erpnext.bundle.css"
|
app_include_css = "erpnext.bundle.css"
|
||||||
|
@ -65,9 +65,10 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus === 1 &&
|
if (
|
||||||
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
|
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")) {
|
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
|
||||||
frm.add_custom_button(__("Return"), function() {
|
frm.add_custom_button(__("Return"), function() {
|
||||||
frm.trigger('make_return_entry');
|
frm.trigger('make_return_entry');
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"default_salary_structure"
|
"default_salary_structure",
|
||||||
|
"currency",
|
||||||
|
"default_base_pay"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -16,14 +18,31 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Default Salary Structure",
|
"label": "Default Salary Structure",
|
||||||
"options": "Salary Structure"
|
"options": "Salary Structure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "default_salary_structure",
|
||||||
|
"fieldname": "default_base_pay",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Default Base Pay",
|
||||||
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "default_salary_structure.currency",
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-26 13:12:07.815330",
|
"modified": "2022-05-06 15:42:10.395508",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Grade",
|
"name": "Employee Grade",
|
||||||
|
"naming_rule": "Set by user",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -65,5 +84,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make new leaves allocated field read only if allocation is created via leave policy assignment
|
|
||||||
// and leave type is earned leave, since these leaves would be allocated via the scheduler
|
|
||||||
if (frm.doc.leave_policy_assignment) {
|
|
||||||
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
|
|
||||||
if (r && cint(r.is_earned_leave))
|
|
||||||
frm.set_df_property("new_leaves_allocated", "read_only", 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
expire_allocation: function(frm) {
|
expire_allocation: function(frm) {
|
||||||
|
@ -254,7 +254,18 @@ class LeaveAllocation(Document):
|
|||||||
# Adding a day to include To Date in the difference
|
# Adding a day to include To Date in the difference
|
||||||
date_difference = date_diff(self.to_date, self.from_date) + 1
|
date_difference = date_diff(self.to_date, self.from_date) + 1
|
||||||
if date_difference < self.total_leaves_allocated:
|
if date_difference < self.total_leaves_allocated:
|
||||||
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
|
if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"):
|
||||||
|
frappe.msgprint(
|
||||||
|
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
|
||||||
|
indicator="orange",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.throw(
|
||||||
|
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
|
||||||
|
exc=OverAllocationError,
|
||||||
|
title=_("Over Allocation"),
|
||||||
|
)
|
||||||
|
|
||||||
def create_leave_ledger_entry(self, submit=True):
|
def create_leave_ledger_entry(self, submit=True):
|
||||||
if self.unused_leaves:
|
if self.unused_leaves:
|
||||||
|
@ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(frappe.ValidationError, doc.save)
|
||||||
|
|
||||||
def test_validation_for_over_allocation(self):
|
def test_validation_for_over_allocation(self):
|
||||||
|
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
|
||||||
|
leave_type.save()
|
||||||
|
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": self.employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": self.employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": leave_type.name,
|
||||||
"from_date": getdate("2015-09-1"),
|
"from_date": getdate("2015-09-1"),
|
||||||
"to_date": getdate("2015-09-30"),
|
"to_date": getdate("2015-09-30"),
|
||||||
"new_leaves_allocated": 35,
|
"new_leaves_allocated": 35,
|
||||||
|
"carry_forward": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# allocated leave more than period
|
# allocated leave more than period
|
||||||
self.assertRaises(OverAllocationError, doc.save)
|
self.assertRaises(OverAllocationError, doc.save)
|
||||||
|
|
||||||
|
leave_type.allow_over_allocation = 1
|
||||||
|
leave_type.save()
|
||||||
|
|
||||||
|
# allows creating a leave allocation with more leave days than period days
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Leave Allocation",
|
||||||
|
"__islocal": 1,
|
||||||
|
"employee": self.employee.name,
|
||||||
|
"employee_name": self.employee.employee_name,
|
||||||
|
"leave_type": leave_type.name,
|
||||||
|
"from_date": getdate("2015-09-1"),
|
||||||
|
"to_date": getdate("2015-09-30"),
|
||||||
|
"new_leaves_allocated": 35,
|
||||||
|
"carry_forward": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
def test_validation_for_over_allocation_post_submission(self):
|
def test_validation_for_over_allocation_post_submission(self):
|
||||||
allocation = frappe.get_doc(
|
allocation = frappe.get_doc(
|
||||||
{
|
{
|
||||||
|
@ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while i < 14:
|
while i < 14:
|
||||||
allocate_earned_leaves(ignore_duplicates=True)
|
allocate_earned_leaves()
|
||||||
i += 1
|
i += 1
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||||
|
|
||||||
@ -753,7 +753,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||||
i = 0
|
i = 0
|
||||||
while i < 6:
|
while i < 6:
|
||||||
allocate_earned_leaves(ignore_duplicates=True)
|
allocate_earned_leaves()
|
||||||
i += 1
|
i += 1
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
||||||
|
|
||||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||||
@ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
|||||||
test_dependencies = ["Employee"]
|
test_dependencies = ["Employee"]
|
||||||
|
|
||||||
|
|
||||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
class TestLeavePolicyAssignment(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for doctype in [
|
for doctype in [
|
||||||
"Leave Period",
|
"Leave Period",
|
||||||
@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
leave_policy = create_leave_policy()
|
leave_policy = create_leave_policy()
|
||||||
leave_policy.submit()
|
leave_policy.submit()
|
||||||
|
|
||||||
|
self.employee.date_of_joining = get_first_day(leave_period.from_date)
|
||||||
|
self.employee.save()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"assignment_based_on": "Leave Period",
|
"assignment_based_on": "Leave Period",
|
||||||
"leave_policy": leave_policy.name,
|
"leave_policy": leave_policy.name,
|
||||||
@ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
self.assertEqual(leaves_allocated, 3)
|
||||||
|
|
||||||
# if the daily job is not completed yet, there is another check present
|
|
||||||
# to ensure leave is not already allocated to avoid duplication
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
allocate_earned_leaves()
|
|
||||||
|
|
||||||
leaves_allocated = frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
|
||||||
"total_leaves_allocated",
|
|
||||||
)
|
|
||||||
self.assertEqual(leaves_allocated, 3)
|
|
||||||
|
|
||||||
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
|
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
|
||||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||||
|
|
||||||
@ -242,20 +233,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
self.assertEqual(details.unused_leaves, 5)
|
self.assertEqual(details.unused_leaves, 5)
|
||||||
self.assertEqual(details.total_leaves_allocated, 7)
|
self.assertEqual(details.total_leaves_allocated, 7)
|
||||||
|
|
||||||
# if the daily job is not completed yet, there is another check present
|
|
||||||
# to ensure leave is not already allocated to avoid duplication
|
|
||||||
from erpnext.hr.utils import is_earned_leave_already_allocated
|
|
||||||
|
|
||||||
frappe.flags.current_date = get_last_day(getdate())
|
|
||||||
|
|
||||||
allocation = frappe.get_doc("Leave Allocation", details.name)
|
|
||||||
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
|
|
||||||
self.assertFalse(
|
|
||||||
is_earned_leave_already_allocated(
|
|
||||||
allocation, leave_policy.leave_policy_details[0].annual_allocation
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
|
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
|
||||||
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
|
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
|
||||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||||
@ -288,19 +265,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
self.assertEqual(leaves_allocated, 3)
|
||||||
|
|
||||||
# to ensure leave is not already allocated to avoid duplication
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
frappe.flags.current_date = get_last_day(getdate())
|
|
||||||
allocate_earned_leaves()
|
|
||||||
|
|
||||||
leaves_allocated = frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
|
||||||
"total_leaves_allocated",
|
|
||||||
)
|
|
||||||
self.assertEqual(leaves_allocated, 3)
|
|
||||||
|
|
||||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
|
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
|
||||||
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
|
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
|
||||||
leave_period, leave_policy = setup_leave_period_and_policy(
|
leave_period, leave_policy = setup_leave_period_and_policy(
|
||||||
@ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
self.assertEqual(leaves_allocated, 3)
|
||||||
|
|
||||||
# if the daily job is not completed yet, there is another check present
|
|
||||||
# to ensure leave is not already allocated to avoid duplication
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
frappe.flags.current_date = get_first_day(getdate())
|
|
||||||
allocate_earned_leaves()
|
|
||||||
|
|
||||||
leaves_allocated = frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
|
||||||
"total_leaves_allocated",
|
|
||||||
)
|
|
||||||
self.assertEqual(leaves_allocated, 3)
|
|
||||||
|
|
||||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
|
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
|
||||||
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
|
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
|
||||||
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
|
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
|
||||||
@ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||||
self.assertEqual(leaves_allocated, 3)
|
self.assertEqual(leaves_allocated, 3)
|
||||||
|
|
||||||
# to ensure leave is not already allocated to avoid duplication
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
frappe.flags.current_date = get_first_day(getdate())
|
|
||||||
allocate_earned_leaves()
|
|
||||||
|
|
||||||
leaves_allocated = frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
|
||||||
"total_leaves_allocated",
|
|
||||||
)
|
|
||||||
self.assertEqual(leaves_allocated, 3)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||||
frappe.flags.current_date = None
|
frappe.flags.current_date = None
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"fraction_of_daily_salary_per_leave",
|
"fraction_of_daily_salary_per_leave",
|
||||||
"is_optional_leave",
|
"is_optional_leave",
|
||||||
"allow_negative",
|
"allow_negative",
|
||||||
|
"allow_over_allocation",
|
||||||
"include_holiday",
|
"include_holiday",
|
||||||
"is_compensatory",
|
"is_compensatory",
|
||||||
"carry_forward_section",
|
"carry_forward_section",
|
||||||
@ -211,15 +212,23 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Fraction of Daily Salary per Leave",
|
"label": "Fraction of Daily Salary per Leave",
|
||||||
"mandatory_depends_on": "eval:doc.is_ppl == 1"
|
"mandatory_depends_on": "eval:doc.is_ppl == 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Allows allocating more leaves than the number of days in the allocation period.",
|
||||||
|
"fieldname": "allow_over_allocation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Over Allocation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-flag",
|
"icon": "fa fa-flag",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-02 11:59:40.503359",
|
"modified": "2022-05-09 05:01:38.957545",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Type",
|
"name": "Leave Type",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -251,5 +260,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -4,21 +4,21 @@
|
|||||||
frappe.query_reports["Employee Leave Balance"] = {
|
frappe.query_reports["Employee Leave Balance"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"from_date",
|
"fieldname": "from_date",
|
||||||
"label": __("From Date"),
|
"label": __("From Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": frappe.defaults.get_default("year_start_date")
|
"default": frappe.defaults.get_default("year_start_date")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_date",
|
"fieldname": "to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": frappe.defaults.get_default("year_end_date")
|
"default": frappe.defaults.get_default("year_end_date")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"company",
|
"fieldname": "company",
|
||||||
"label": __("Company"),
|
"label": __("Company"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@ -26,16 +26,29 @@ frappe.query_reports["Employee Leave Balance"] = {
|
|||||||
"default": frappe.defaults.get_user_default("Company")
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"department",
|
"fieldname": "department",
|
||||||
"label": __("Department"),
|
"label": __("Department"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Department",
|
"options": "Department",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"employee",
|
"fieldname": "employee",
|
||||||
"label": __("Employee"),
|
"label": __("Employee"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Employee",
|
"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",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -168,9 +168,8 @@ def get_opening_balance(
|
|||||||
|
|
||||||
|
|
||||||
def get_conditions(filters: Filters) -> Dict:
|
def get_conditions(filters: Filters) -> Dict:
|
||||||
conditions = {
|
conditions = {}
|
||||||
"status": "Active",
|
|
||||||
}
|
|
||||||
if filters.get("employee"):
|
if filters.get("employee"):
|
||||||
conditions["name"] = filters.get("employee")
|
conditions["name"] = filters.get("employee")
|
||||||
|
|
||||||
@ -180,6 +179,9 @@ def get_conditions(filters: Filters) -> Dict:
|
|||||||
if filters.get("department"):
|
if filters.get("department"):
|
||||||
conditions["department"] = filters.get("department")
|
conditions["department"] = filters.get("department")
|
||||||
|
|
||||||
|
if filters.get("employee_status"):
|
||||||
|
conditions["status"] = filters.get("employee_status")
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
|
@ -207,3 +207,40 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
allocation1.new_leaves_allocated - leave_application.total_leave_days
|
allocation1.new_leaves_allocated - leave_application.total_leave_days
|
||||||
)
|
)
|
||||||
self.assertEqual(report[1][0].opening_balance, opening_balance)
|
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)
|
||||||
|
@ -30,6 +30,19 @@ frappe.query_reports['Employee Leave Balance Summary'] = {
|
|||||||
label: __('Department'),
|
label: __('Department'),
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
options: 'Department',
|
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",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -35,9 +35,10 @@ def get_columns(leave_types):
|
|||||||
|
|
||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = {
|
conditions = {
|
||||||
"status": "Active",
|
|
||||||
"company": filters.company,
|
"company": filters.company,
|
||||||
}
|
}
|
||||||
|
if filters.get("employee_status"):
|
||||||
|
conditions.update({"status": filters.get("employee_status")})
|
||||||
if filters.get("department"):
|
if filters.get("department"):
|
||||||
conditions.update({"department": filters.get("department")})
|
conditions.update({"department": filters.get("department")})
|
||||||
if filters.get("employee"):
|
if filters.get("employee"):
|
||||||
|
@ -36,7 +36,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.set_user("Administrator")
|
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.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
||||||
|
|
||||||
self.date = getdate()
|
self.date = getdate()
|
||||||
@ -146,3 +145,37 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(report[1], expected_data)
|
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)
|
||||||
|
@ -269,7 +269,7 @@ def generate_leave_encashment():
|
|||||||
create_leave_encashment(leave_allocation=leave_allocation)
|
create_leave_encashment(leave_allocation=leave_allocation)
|
||||||
|
|
||||||
|
|
||||||
def allocate_earned_leaves(ignore_duplicates=False):
|
def allocate_earned_leaves():
|
||||||
"""Allocate earned leaves to Employees"""
|
"""Allocate earned leaves to Employees"""
|
||||||
e_leave_types = get_earned_leaves()
|
e_leave_types = get_earned_leaves()
|
||||||
today = getdate()
|
today = getdate()
|
||||||
@ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False):
|
|||||||
if check_effective_date(
|
if check_effective_date(
|
||||||
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
|
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
|
||||||
):
|
):
|
||||||
update_previous_leave_allocation(
|
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
|
||||||
allocation, annual_allocation, e_leave_type, ignore_duplicates
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_previous_leave_allocation(
|
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||||
allocation, annual_allocation, e_leave_type, ignore_duplicates=False
|
|
||||||
):
|
|
||||||
earned_leaves = get_monthly_earned_leave(
|
earned_leaves = get_monthly_earned_leave(
|
||||||
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
||||||
)
|
)
|
||||||
@ -326,20 +322,19 @@ def update_previous_leave_allocation(
|
|||||||
if new_allocation != allocation.total_leaves_allocated:
|
if new_allocation != allocation.total_leaves_allocated:
|
||||||
today_date = today()
|
today_date = today()
|
||||||
|
|
||||||
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
|
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
|
||||||
|
|
||||||
if e_leave_type.based_on_date_of_joining:
|
if e_leave_type.based_on_date_of_joining:
|
||||||
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
|
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
|
||||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
||||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||||
)
|
)
|
||||||
|
|
||||||
allocation.add_comment(comment_type="Info", text=text)
|
allocation.add_comment(comment_type="Info", text=text)
|
||||||
|
|
||||||
|
|
||||||
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||||
|
@ -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))
|
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.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_missing_values()
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
|
|
||||||
|
@ -313,7 +313,6 @@ erpnext.patches.v13_0.enable_uoms
|
|||||||
erpnext.patches.v12_0.update_production_plan_status
|
erpnext.patches.v12_0.update_production_plan_status
|
||||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
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.update_category_in_ltds_certificate
|
||||||
erpnext.patches.v13_0.create_pan_field_for_india #2
|
erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||||
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
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.wipe_serial_no_field_for_0_qty
|
||||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||||
erpnext.patches.v13_0.update_tax_category_for_rcm
|
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.v14_0.set_payroll_cost_centers
|
||||||
erpnext.patches.v13_0.agriculture_deprecation_warning
|
erpnext.patches.v13_0.agriculture_deprecation_warning
|
||||||
erpnext.patches.v13_0.hospitality_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.enable_provisional_accounting
|
||||||
erpnext.patches.v13_0.non_profit_deprecation_warning
|
erpnext.patches.v13_0.non_profit_deprecation_warning
|
||||||
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||||
erpnext.patches.v14_0.delete_education_doctypes
|
|
||||||
|
|
||||||
[post_model_sync]
|
[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.rename_ongoing_status_in_sla_documents
|
||||||
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
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_hub_doctypes
|
||||||
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
|
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
|
||||||
erpnext.patches.v14_0.delete_agriculture_doctypes
|
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.delete_datev_doctypes
|
||||||
erpnext.patches.v14_0.rearrange_company_fields
|
erpnext.patches.v14_0.rearrange_company_fields
|
||||||
erpnext.patches.v14_0.update_leave_notification_template
|
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.v13_0.change_default_item_manufacturer_fieldtype
|
||||||
erpnext.patches.v14_0.discount_accounting_separation
|
erpnext.patches.v14_0.discount_accounting_separation
|
||||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
||||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||||
|
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
@ -33,7 +33,7 @@ def execute():
|
|||||||
"insert_after": insert_after_field,
|
"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)
|
frappe.clear_cache(doctype=doctype)
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
dn = frappe.qb.DocType("Delivery Note")
|
||||||
|
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||||
|
|
||||||
|
dn_list = (
|
||||||
|
frappe.qb.from_(dn)
|
||||||
|
.inner_join(dn_item)
|
||||||
|
.on(dn.name == dn_item.parent)
|
||||||
|
.select(dn.name)
|
||||||
|
.where(dn.docstatus == 1)
|
||||||
|
.where(dn.is_return == 1)
|
||||||
|
.where(dn.per_billed < 100)
|
||||||
|
.where(dn_item.returned_qty > 0)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
|
||||||
|
dn_item.returned_qty, 0
|
||||||
|
).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
|
||||||
|
|
||||||
|
for d in dn_list:
|
||||||
|
dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
|
||||||
|
dn_doc.run_method("update_billing_status")
|
@ -164,6 +164,15 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
primary_action_label: __('Assign')
|
primary_action_label: __('Assign')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
d.fields_dict.grade.df.onchange = function() {
|
||||||
|
const grade = d.fields_dict.grade.value;
|
||||||
|
if (grade) {
|
||||||
|
frappe.db.get_value('Employee Grade', grade, 'default_base_pay')
|
||||||
|
.then(({ message }) => {
|
||||||
|
d.set_value('base', message.default_base_pay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
d.show();
|
d.show();
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_years, date_diff, get_first_day, nowdate
|
from frappe.utils import add_years, date_diff, get_first_day, nowdate
|
||||||
from frappe.utils.make_random import get_random
|
from frappe.utils.make_random import get_random
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
|
|||||||
test_dependencies = ["Fiscal Year"]
|
test_dependencies = ["Fiscal Year"]
|
||||||
|
|
||||||
|
|
||||||
class TestSalaryStructure(unittest.TestCase):
|
class TestSalaryStructure(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]:
|
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]:
|
||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
frappe.db.sql("delete from `tab%s`" % dt)
|
||||||
@ -132,6 +133,23 @@ class TestSalaryStructure(unittest.TestCase):
|
|||||||
self.assertEqual(salary_structure_assignment.base, 5000)
|
self.assertEqual(salary_structure_assignment.base, 5000)
|
||||||
self.assertEqual(salary_structure_assignment.variable, 200)
|
self.assertEqual(salary_structure_assignment.variable, 200)
|
||||||
|
|
||||||
|
def test_employee_grade_defaults(self):
|
||||||
|
salary_structure = make_salary_structure(
|
||||||
|
"Salary Structure - Lead", "Monthly", currency="INR", company="_Test Company"
|
||||||
|
)
|
||||||
|
create_employee_grade("Lead", salary_structure.name)
|
||||||
|
employee = make_employee("test_employee_grade@salary.com", company="_Test Company", grade="Lead")
|
||||||
|
|
||||||
|
# structure assignment should have the default salary structure and base pay
|
||||||
|
salary_structure.assign_salary_structure(employee=employee, from_date=nowdate())
|
||||||
|
structure, base = frappe.db.get_value(
|
||||||
|
"Salary Structure Assignment",
|
||||||
|
{"employee": employee, "salary_structure": salary_structure.name, "from_date": nowdate()},
|
||||||
|
["salary_structure", "base"],
|
||||||
|
)
|
||||||
|
self.assertEqual(structure, salary_structure.name)
|
||||||
|
self.assertEqual(base, 50000)
|
||||||
|
|
||||||
def test_multi_currency_salary_structure(self):
|
def test_multi_currency_salary_structure(self):
|
||||||
make_employee("test_muti_currency_employee@salary.com")
|
make_employee("test_muti_currency_employee@salary.com")
|
||||||
sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency="USD")
|
sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency="USD")
|
||||||
@ -251,3 +269,15 @@ def get_payable_account(company=None):
|
|||||||
if not company:
|
if not company:
|
||||||
company = erpnext.get_default_company()
|
company = erpnext.get_default_company()
|
||||||
return frappe.db.get_value("Company", company, "default_payroll_payable_account")
|
return frappe.db.get_value("Company", company, "default_payroll_payable_account")
|
||||||
|
|
||||||
|
|
||||||
|
def create_employee_grade(grade, default_structure=None):
|
||||||
|
if not frappe.db.exists("Employee Grade", grade):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Grade",
|
||||||
|
"__newname": grade,
|
||||||
|
"default_salary_structure": default_structure,
|
||||||
|
"default_base_pay": 50000,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"employee",
|
"employee",
|
||||||
"employee_name",
|
"employee_name",
|
||||||
"department",
|
"department",
|
||||||
|
"grade",
|
||||||
"company",
|
"company",
|
||||||
"payroll_payable_account",
|
"payroll_payable_account",
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
@ -67,6 +68,8 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "grade.default_salary_structure",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "salary_structure",
|
"fieldname": "salary_structure",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -96,6 +99,8 @@
|
|||||||
"label": "Base & Variable"
|
"label": "Base & Variable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "grade.default_base_pay",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "base",
|
"fieldname": "base",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Base",
|
"label": "Base",
|
||||||
@ -158,11 +163,19 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Cost Centers",
|
"label": "Cost Centers",
|
||||||
"options": "Employee Cost Center"
|
"options": "Employee Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "employee.grade",
|
||||||
|
"fieldname": "grade",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Grade",
|
||||||
|
"options": "Employee Grade",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-19 12:43:54.439073",
|
"modified": "2022-05-06 12:18:36.972336",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Structure Assignment",
|
"name": "Salary Structure Assignment",
|
||||||
|
@ -27,7 +27,8 @@ frappe.ui.form.on(cur_frm.doctype, {
|
|||||||
query: "erpnext.controllers.queries.tax_account_query",
|
query: "erpnext.controllers.queries.tax_account_query",
|
||||||
filters: {
|
filters: {
|
||||||
"account_type": account_type,
|
"account_type": account_type,
|
||||||
"company": doc.company
|
"company": doc.company,
|
||||||
|
"disabled": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -923,12 +923,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
currency() {
|
currency() {
|
||||||
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */
|
// The transaction date be either transaction_date (from orders) or posting_date (from invoices)
|
||||||
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
||||||
/* end manqala */
|
|
||||||
var me = this;
|
let me = this;
|
||||||
this.set_dynamic_labels();
|
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
|
// 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
|
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
||||||
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
||||||
@ -942,7 +942,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} 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 me = this;
|
||||||
var args = this._get_args(item);
|
var args = this._get_args(item);
|
||||||
if (!(args.items && args.items.length)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target doc created from a mapped doc
|
// Target doc created from a mapped doc
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
|
|||||||
filters.splice(index, 0, {
|
filters.splice(index, 0, {
|
||||||
"fieldname": dimension["fieldname"],
|
"fieldname": dimension["fieldname"],
|
||||||
"label": __(dimension["label"]),
|
"label": __(dimension["label"]),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "MultiSelectList",
|
||||||
"options": dimension["document_type"]
|
get_data: function(txt) {
|
||||||
|
return frappe.db.get_link_options(dimension["document_type"], txt);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
this.serial_no_field = opts.serial_no_field || "serial_no";
|
this.serial_no_field = opts.serial_no_field || "serial_no";
|
||||||
this.batch_no_field = opts.batch_no_field || "batch_no";
|
this.batch_no_field = opts.batch_no_field || "batch_no";
|
||||||
this.qty_field = opts.qty_field || "qty";
|
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_name = opts.items_table_name || "items";
|
||||||
this.items_table = this.frm.doc[this.items_table_name];
|
this.items_table = this.frm.doc[this.items_table_name];
|
||||||
@ -42,10 +48,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
.then((r) => {
|
.then((r) => {
|
||||||
const data = r && r.message;
|
const data = r && r.message;
|
||||||
if (!data || Object.keys(data).length === 0) {
|
if (!data || Object.keys(data).length === 0) {
|
||||||
frappe.show_alert({
|
this.show_alert(__("Cannot find Item with this Barcode"), "red");
|
||||||
message: __("Cannot find Item with this Barcode"),
|
|
||||||
indicator: "red",
|
|
||||||
});
|
|
||||||
this.clean_up();
|
this.clean_up();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -56,22 +59,18 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
|
|
||||||
update_table(data) {
|
update_table(data) {
|
||||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||||
let row = null;
|
|
||||||
|
|
||||||
const {item_code, barcode, batch_no, serial_no} = data;
|
const {item_code, barcode, batch_no, serial_no} = data;
|
||||||
|
|
||||||
// Check if batch is scanned and table has batch no field
|
let row = this.get_row_to_modify_on_scan(item_code, batch_no);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row) {
|
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
|
// add new row if new item/batch is scanned
|
||||||
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
||||||
// trigger any row add triggers defined on child table.
|
// trigger any row add triggers defined on child table.
|
||||||
@ -83,9 +82,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.show_scan_message(row.idx, row.item_code);
|
|
||||||
this.set_selector_trigger_flag(row, data);
|
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_serial_no(row, serial_no);
|
||||||
this.set_batch_no(row, batch_no);
|
this.set_batch_no(row, batch_no);
|
||||||
this.set_barcode(row, barcode);
|
this.set_barcode(row, barcode);
|
||||||
@ -106,9 +106,23 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set_item(row, item_code) {
|
set_item(row, item_code) {
|
||||||
const item_data = { item_code: item_code };
|
return new Promise(resolve => {
|
||||||
item_data[this.qty_field] = (row[this.qty_field] || 0) + 1;
|
const increment = (value = 1) => {
|
||||||
frappe.model.set_value(row.doctype, row.name, item_data);
|
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) {
|
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
|
// show new row or qty increase toast
|
||||||
if (exist) {
|
if (exist) {
|
||||||
frappe.show_alert(
|
this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
|
||||||
{
|
|
||||||
message: __("Row #{0}: Qty increased by 1", [idx]),
|
|
||||||
indicator: "green",
|
|
||||||
},
|
|
||||||
5
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
frappe.show_alert(
|
this.show_alert(__("Row #{0}: Item added", [idx]), "green")
|
||||||
{
|
|
||||||
message: __("Row #{0}: Item added", [idx]),
|
|
||||||
indicator: "green",
|
|
||||||
},
|
|
||||||
5
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is_duplicate_serial_no(row, serial_no) {
|
is_duplicate_serial_no(row, serial_no) {
|
||||||
const is_duplicate = !!serial_no && !!row[this.serial_no_field]
|
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
|
||||||
&& row[this.serial_no_field].includes(serial_no);
|
|
||||||
|
|
||||||
if (is_duplicate) {
|
if (is_duplicate) {
|
||||||
frappe.show_alert(
|
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||||
{
|
|
||||||
message: __("Serial No {0} is already added", [serial_no]),
|
|
||||||
indicator: "orange",
|
|
||||||
},
|
|
||||||
5
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return is_duplicate;
|
return is_duplicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_batch_row_to_modify(batch_no) {
|
get_row_to_modify_on_scan(item_code, batch_no) {
|
||||||
// get row if batch already exists in table
|
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||||
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) {
|
// Check if batch is scanned and table has batch no field
|
||||||
// get an existing item row to increment or blank row to modify
|
let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
|
||||||
const existing_item_row = this.items_table.find((d) => d.item_code === item_code);
|
let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);
|
||||||
return existing_item_row || this.get_existing_blank_row();
|
|
||||||
|
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() {
|
get_existing_blank_row() {
|
||||||
@ -194,4 +197,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
this.scan_barcode_field.set_value("");
|
this.scan_barcode_field.set_value("");
|
||||||
refresh_field(this.items_table_name);
|
refresh_field(this.items_table_name);
|
||||||
}
|
}
|
||||||
|
show_alert(msg, indicator, duration=3) {
|
||||||
|
frappe.show_alert({message: msg, indicator: indicator}, duration);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
...data
|
...data
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: () => frm.reload_doc() || d.hide(),
|
callback: () => {
|
||||||
error: () => d.hide()
|
frappe.show_alert({
|
||||||
|
message: __('E-Way Bill Generated successfully'),
|
||||||
|
indicator: 'green'
|
||||||
|
}, 7);
|
||||||
|
frm.reload_doc();
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('E-Way Bill was not Generated'),
|
||||||
|
indicator: 'red'
|
||||||
|
}, 7);
|
||||||
|
d.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
primary_action_label: __('Submit')
|
primary_action_label: __('Submit')
|
||||||
@ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
"label": "Reason",
|
||||||
|
"fieldname": "reason",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": "1-Duplicate",
|
||||||
|
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Remark",
|
||||||
|
"fieldname": "remark",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
];
|
||||||
const action = () => {
|
const action = () => {
|
||||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
const d = new frappe.ui.Dialog({
|
||||||
message += '<br><br>';
|
title: __('Cancel E-Way Bill'),
|
||||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
fields: fields,
|
||||||
|
primary_action: function() {
|
||||||
|
const data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||||
|
args: {
|
||||||
|
doctype,
|
||||||
|
docname: name,
|
||||||
|
eway_bill: ewaybill,
|
||||||
|
reason: data.reason.split('-')[0],
|
||||||
|
remark: data.remark
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: () => {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('E-Way Bill Cancelled successfully'),
|
||||||
|
indicator: 'green'
|
||||||
|
}, 7);
|
||||||
|
frm.reload_doc();
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('E-Way Bill was not Cancelled'),
|
||||||
|
indicator: 'red'
|
||||||
|
}, 7);
|
||||||
|
d.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __('Submit')
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (irn && !irn_cancelled) {
|
||||||
|
const action = () => {
|
||||||
const dialog = frappe.msgprint({
|
const dialog = frappe.msgprint({
|
||||||
title: __('Update E-Way Bill Cancelled Status?'),
|
title: __("Generate QRCode"),
|
||||||
message: message,
|
message: __("Generate and attach QR Code using IRN?"),
|
||||||
indicator: 'orange',
|
|
||||||
primary_action: {
|
primary_action: {
|
||||||
action: function() {
|
action: function() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
||||||
args: { doctype, docname: name },
|
args: { doctype, docname: name },
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: () => frm.reload_doc() || dialog.hide()
|
callback: () => frm.reload_doc() || dialog.hide(),
|
||||||
|
error: () => dialog.hide()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primary_action_label: __('Yes')
|
primary_action_label: __('Yes')
|
||||||
});
|
});
|
||||||
|
dialog.show();
|
||||||
};
|
};
|
||||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
add_custom_button(__("Generate QRCode"), action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -167,85 +234,100 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
const get_ewaybill_fields = (frm) => {
|
const get_ewaybill_fields = (frm) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter',
|
fieldname: "eway_part_a_section_break",
|
||||||
'label': 'Transporter',
|
fieldtype: "Section Break",
|
||||||
'fieldtype': 'Link',
|
label: "Part A",
|
||||||
'options': 'Supplier',
|
|
||||||
'default': frm.doc.transporter
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'gst_transporter_id',
|
fieldname: "transporter",
|
||||||
'label': 'GST Transporter ID',
|
label: "Transporter",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Link",
|
||||||
'default': frm.doc.gst_transporter_id
|
options: "Supplier",
|
||||||
|
default: frm.doc.transporter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'driver',
|
fieldname: "transporter_name",
|
||||||
'label': 'Driver',
|
label: "Transporter Name",
|
||||||
'fieldtype': 'Link',
|
fieldtype: "Data",
|
||||||
'options': 'Driver',
|
read_only: 1,
|
||||||
'default': frm.doc.driver
|
default: frm.doc.transporter_name,
|
||||||
|
depends_on: "transporter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'lr_no',
|
fieldname: "part_a_column_break",
|
||||||
'label': 'Transport Receipt No',
|
fieldtype: "Column Break",
|
||||||
'fieldtype': 'Data',
|
|
||||||
'default': frm.doc.lr_no
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'vehicle_no',
|
fieldname: "gst_transporter_id",
|
||||||
'label': 'Vehicle No',
|
label: "GST Transporter ID",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Data",
|
||||||
'default': frm.doc.vehicle_no
|
default: frm.doc.gst_transporter_id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'distance',
|
fieldname: "distance",
|
||||||
'label': 'Distance (in km)',
|
label: "Distance (in km)",
|
||||||
'fieldtype': 'Float',
|
fieldtype: "Float",
|
||||||
'default': frm.doc.distance
|
default: frm.doc.distance,
|
||||||
|
description: 'Set as zero to auto calculate distance using pin codes',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter_col_break',
|
fieldname: "eway_part_b_section_break",
|
||||||
'fieldtype': 'Column Break',
|
fieldtype: "Section Break",
|
||||||
|
label: "Part B",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'transporter_name',
|
fieldname: "mode_of_transport",
|
||||||
'label': 'Transporter Name',
|
label: "Mode of Transport",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Select",
|
||||||
'read_only': 1,
|
options: `\nRoad\nAir\nRail\nShip`,
|
||||||
'default': frm.doc.transporter_name,
|
default: frm.doc.mode_of_transport,
|
||||||
'depends_on': 'transporter'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'mode_of_transport',
|
fieldname: "gst_vehicle_type",
|
||||||
'label': 'Mode of Transport',
|
label: "GST Vehicle Type",
|
||||||
'fieldtype': 'Select',
|
fieldtype: "Select",
|
||||||
'options': `\nRoad\nAir\nRail\nShip`,
|
options: `Regular\nOver Dimensional Cargo (ODC)`,
|
||||||
'default': frm.doc.mode_of_transport
|
depends_on: 'eval:(doc.mode_of_transport === "Road")',
|
||||||
|
default: frm.doc.gst_vehicle_type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'driver_name',
|
fieldname: "vehicle_no",
|
||||||
'label': 'Driver Name',
|
label: "Vehicle No",
|
||||||
'fieldtype': 'Data',
|
fieldtype: "Data",
|
||||||
'fetch_from': 'driver.full_name',
|
default: frm.doc.vehicle_no,
|
||||||
'read_only': 1,
|
|
||||||
'default': frm.doc.driver_name,
|
|
||||||
'depends_on': 'driver'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'lr_date',
|
fieldname: "part_b_column_break",
|
||||||
'label': 'Transport Receipt Date',
|
fieldtype: "Column Break",
|
||||||
'fieldtype': 'Date',
|
|
||||||
'default': frm.doc.lr_date
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'gst_vehicle_type',
|
fieldname: "lr_date",
|
||||||
'label': 'GST Vehicle Type',
|
label: "Transport Receipt Date",
|
||||||
'fieldtype': 'Select',
|
fieldtype: "Date",
|
||||||
'options': `Regular\nOver Dimensional Cargo (ODC)`,
|
default: frm.doc.lr_date,
|
||||||
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
},
|
||||||
'default': frm.doc.gst_vehicle_type
|
{
|
||||||
}
|
fieldname: "lr_no",
|
||||||
|
label: "Transport Receipt No",
|
||||||
|
fieldtype: "Data",
|
||||||
|
default: frm.doc.lr_no,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "driver",
|
||||||
|
label: "Driver",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Driver",
|
||||||
|
default: frm.doc.driver,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "driver_name",
|
||||||
|
label: "Driver Name",
|
||||||
|
fieldtype: "Data",
|
||||||
|
fetch_from: "driver.full_name",
|
||||||
|
read_only: 1,
|
||||||
|
default: frm.doc.driver_name,
|
||||||
|
depends_on: "driver",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +167,12 @@ def get_doc_details(invoice):
|
|||||||
title=_("Not Allowed"),
|
title=_("Not Allowed"),
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice_type = "CRN" if invoice.is_return else "INV"
|
if invoice.is_return:
|
||||||
|
invoice_type = "CRN"
|
||||||
|
elif invoice.is_debit_note:
|
||||||
|
invoice_type = "DBN"
|
||||||
|
else:
|
||||||
|
invoice_type = "INV"
|
||||||
|
|
||||||
invoice_name = invoice.name
|
invoice_name = invoice.name
|
||||||
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
|
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
|
||||||
@ -443,7 +448,7 @@ def get_eway_bill_details(invoice):
|
|||||||
dict(
|
dict(
|
||||||
gstin=invoice.gst_transporter_id,
|
gstin=invoice.gst_transporter_id,
|
||||||
name=invoice.transporter_name,
|
name=invoice.transporter_name,
|
||||||
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
|
mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None,
|
||||||
distance=invoice.distance or 0,
|
distance=invoice.distance or 0,
|
||||||
document_name=invoice.lr_no,
|
document_name=invoice.lr_no,
|
||||||
document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
|
document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
|
||||||
@ -792,8 +797,9 @@ class GSPConnector:
|
|||||||
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
|
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
|
||||||
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
||||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
|
||||||
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
||||||
|
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
||||||
|
|
||||||
def set_invoice(self):
|
def set_invoice(self):
|
||||||
self.invoice = None
|
self.invoice = None
|
||||||
@ -857,8 +863,8 @@ class GSPConnector:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def auto_refresh_token(self):
|
def auto_refresh_token(self):
|
||||||
self.fetch_auth_token()
|
|
||||||
self.token_auto_refreshed = True
|
self.token_auto_refreshed = True
|
||||||
|
self.fetch_auth_token()
|
||||||
|
|
||||||
def log_request(self, url, headers, data, res):
|
def log_request(self, url, headers, data, res):
|
||||||
headers.update({"password": self.credentials.password})
|
headers.update({"password": self.credentials.password})
|
||||||
@ -998,6 +1004,37 @@ class GSPConnector:
|
|||||||
|
|
||||||
return failed
|
return failed
|
||||||
|
|
||||||
|
def fetch_and_attach_qrcode_from_irn(self):
|
||||||
|
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
|
||||||
|
if qrcode:
|
||||||
|
qrcode_file = self.create_qr_code_file(qrcode)
|
||||||
|
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
|
||||||
|
frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
|
||||||
|
|
||||||
|
def get_qrcode_from_irn(self, irn):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
headers = self.get_headers()
|
||||||
|
headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# using requests.get instead of make_request to avoid parsing the response
|
||||||
|
res = requests.get(self.get_qrcode_url, headers=headers)
|
||||||
|
self.log_request(self.get_qrcode_url, headers, None, None)
|
||||||
|
if res.status_code == 200:
|
||||||
|
return res.content
|
||||||
|
else:
|
||||||
|
raise RequestFailed(str(res.content, "utf-8"))
|
||||||
|
|
||||||
|
except RequestFailed as e:
|
||||||
|
self.raise_error(errors=str(e))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log_error()
|
||||||
|
self.raise_error()
|
||||||
|
|
||||||
def get_irn_details(self, irn):
|
def get_irn_details(self, irn):
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
|
|
||||||
@ -1113,6 +1150,19 @@ class GSPConnector:
|
|||||||
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
|
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
|
||||||
self.invoice.eway_bill_cancelled = 0
|
self.invoice.eway_bill_cancelled = 0
|
||||||
self.invoice.update(args)
|
self.invoice.update(args)
|
||||||
|
if res.get("info"):
|
||||||
|
info = res.get("info")
|
||||||
|
# when we have more features (responses) in eway bill, we can add them using below forloop.
|
||||||
|
for msg in info:
|
||||||
|
if msg.get("InfCd") == "EWBPPD":
|
||||||
|
pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group())
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)),
|
||||||
|
title="Notification",
|
||||||
|
indicator="green",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
self.invoice.distance = flt(pin_to_pin_distance)
|
||||||
self.invoice.flags.updater_reference = {
|
self.invoice.flags.updater_reference = {
|
||||||
"doctype": self.invoice.doctype,
|
"doctype": self.invoice.doctype,
|
||||||
"docname": self.invoice.name,
|
"docname": self.invoice.name,
|
||||||
@ -1135,7 +1185,6 @@ class GSPConnector:
|
|||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||||
headers["username"] = headers["user_name"]
|
headers["username"] = headers["user_name"]
|
||||||
del headers["user_name"]
|
|
||||||
try:
|
try:
|
||||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||||
if res.get("success"):
|
if res.get("success"):
|
||||||
@ -1186,8 +1235,6 @@ class GSPConnector:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
def raise_error(self, raise_exception=False, errors=None):
|
def raise_error(self, raise_exception=False, errors=None):
|
||||||
if errors is None:
|
|
||||||
errors = []
|
|
||||||
title = _("E Invoice Request Failed")
|
title = _("E Invoice Request Failed")
|
||||||
if errors:
|
if errors:
|
||||||
frappe.throw(errors, title=title, as_list=1)
|
frappe.throw(errors, title=title, as_list=1)
|
||||||
@ -1228,13 +1275,18 @@ class GSPConnector:
|
|||||||
|
|
||||||
def attach_qrcode_image(self):
|
def attach_qrcode_image(self):
|
||||||
qrcode = self.invoice.signed_qr_code
|
qrcode = self.invoice.signed_qr_code
|
||||||
doctype = self.invoice.doctype
|
|
||||||
docname = self.invoice.name
|
|
||||||
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
|
|
||||||
|
|
||||||
qr_image = io.BytesIO()
|
qr_image = io.BytesIO()
|
||||||
url = qrcreate(qrcode, error="L")
|
url = qrcreate(qrcode, error="L")
|
||||||
url.png(qr_image, scale=2, quiet_zone=1)
|
url.png(qr_image, scale=2, quiet_zone=1)
|
||||||
|
qrcode_file = self.create_qr_code_file(qr_image.getvalue())
|
||||||
|
self.invoice.qrcode_image = qrcode_file.file_url
|
||||||
|
|
||||||
|
def create_qr_code_file(self, qr_image):
|
||||||
|
doctype = self.invoice.doctype
|
||||||
|
docname = self.invoice.name
|
||||||
|
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
|
||||||
|
|
||||||
_file = frappe.get_doc(
|
_file = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "File",
|
"doctype": "File",
|
||||||
@ -1243,12 +1295,12 @@ class GSPConnector:
|
|||||||
"attached_to_name": docname,
|
"attached_to_name": docname,
|
||||||
"attached_to_field": "qrcode_image",
|
"attached_to_field": "qrcode_image",
|
||||||
"is_private": 0,
|
"is_private": 0,
|
||||||
"content": qr_image.getvalue(),
|
"content": qr_image,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_file.save()
|
_file.save()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
self.invoice.qrcode_image = _file.file_url
|
return _file
|
||||||
|
|
||||||
def update_invoice(self):
|
def update_invoice(self):
|
||||||
self.invoice.flags.ignore_validate_update_after_submit = True
|
self.invoice.flags.ignore_validate_update_after_submit = True
|
||||||
@ -1293,6 +1345,12 @@ def cancel_irn(doctype, docname, irn, reason, remark):
|
|||||||
gsp_connector.cancel_irn(irn, reason, remark)
|
gsp_connector.cancel_irn(irn, reason, remark)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def generate_qrcode(doctype, docname):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.fetch_and_attach_qrcode_from_irn()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_eway_bill(doctype, docname, **kwargs):
|
def generate_eway_bill(doctype, docname, **kwargs):
|
||||||
gsp_connector = GSPConnector(doctype, docname)
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
@ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_eway_bill(doctype, docname):
|
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||||
# TODO: uncomment when eway_bill api from Adequare is enabled
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
# gsp_connector = GSPConnector(doctype, docname)
|
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
|
||||||
|
|
||||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
|
||||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -32,7 +32,7 @@ def _execute(filters=None):
|
|||||||
added_item = []
|
added_item = []
|
||||||
for d in item_list:
|
for d in item_list:
|
||||||
if (d.parent, d.item_code) not in added_item:
|
if (d.parent, d.item_code) not in added_item:
|
||||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
|
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
|
||||||
total_tax = 0
|
total_tax = 0
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||||
@ -40,11 +40,9 @@ def _execute(filters=None):
|
|||||||
|
|
||||||
row += [d.base_net_amount + total_tax]
|
row += [d.base_net_amount + total_tax]
|
||||||
row += [d.base_net_amount]
|
row += [d.base_net_amount]
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||||
row += [item_tax.get("tax_amount", 0)]
|
row += [item_tax.get("tax_amount", 0)]
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
added_item.append((d.parent, d.item_code))
|
added_item.append((d.parent, d.item_code))
|
||||||
if data:
|
if data:
|
||||||
@ -64,6 +62,7 @@ def get_columns():
|
|||||||
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
|
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
|
||||||
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
|
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
|
||||||
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
|
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
|
||||||
|
{"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90},
|
||||||
{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
|
{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
|
||||||
{
|
{
|
||||||
"fieldname": "taxable_amount",
|
"fieldname": "taxable_amount",
|
||||||
@ -106,16 +105,25 @@ def get_items(filters):
|
|||||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
`tabSales Invoice Item`.parent,
|
||||||
`tabGST HSN Code`.description
|
`tabSales Invoice Item`.item_code,
|
||||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
`tabGST HSN Code`.description,
|
||||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||||
|
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
|
||||||
|
from
|
||||||
|
`tabSales Invoice`,
|
||||||
|
`tabSales Invoice Item`,
|
||||||
|
`tabGST HSN Code`,
|
||||||
|
`tabSales Taxes and Charges`
|
||||||
|
where
|
||||||
|
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||||
|
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||||
and `tabSales Invoice`.docstatus = 1
|
and `tabSales Invoice`.docstatus = 1
|
||||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||||
group by
|
group by
|
||||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
|
`tabSales Invoice Item`.parent,
|
||||||
|
`tabSales Invoice Item`.item_code
|
||||||
"""
|
"""
|
||||||
% (conditions, match_conditions),
|
% (conditions, match_conditions),
|
||||||
filters,
|
filters,
|
||||||
@ -213,15 +221,16 @@ def get_merged_data(columns, data):
|
|||||||
result = []
|
result = []
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
merged_hsn_dict.setdefault(row[0], {})
|
key = row[0] + "-" + str(row[4])
|
||||||
|
merged_hsn_dict.setdefault(key, {})
|
||||||
for i, d in enumerate(columns):
|
for i, d in enumerate(columns):
|
||||||
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
||||||
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
|
merged_hsn_dict[key][d["fieldname"]] = row[i]
|
||||||
else:
|
else:
|
||||||
if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""):
|
if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""):
|
||||||
merged_hsn_dict[row[0]][d["fieldname"]] += row[i]
|
merged_hsn_dict[key][d["fieldname"]] += row[i]
|
||||||
else:
|
else:
|
||||||
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
|
merged_hsn_dict[key][d["fieldname"]] = row[i]
|
||||||
|
|
||||||
for key, value in merged_hsn_dict.items():
|
for key, value in merged_hsn_dict.items():
|
||||||
result.append(value)
|
result.append(value)
|
||||||
@ -240,7 +249,7 @@ def get_json(filters, report_name, data):
|
|||||||
|
|
||||||
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
||||||
|
|
||||||
gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp}
|
gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp}
|
||||||
|
|
||||||
gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)}
|
gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)}
|
||||||
|
|
||||||
@ -271,7 +280,7 @@ def get_hsn_wise_json_data(filters, report_data):
|
|||||||
"desc": hsn.get("description"),
|
"desc": hsn.get("description"),
|
||||||
"uqc": hsn.get("stock_uom").upper(),
|
"uqc": hsn.get("stock_uom").upper(),
|
||||||
"qty": hsn.get("stock_qty"),
|
"qty": hsn.get("stock_qty"),
|
||||||
"val": flt(hsn.get("total_amount"), 2),
|
"rt": flt(hsn.get("tax_rate"), 2),
|
||||||
"txval": flt(hsn.get("taxable_amount", 2)),
|
"txval": flt(hsn.get("taxable_amount", 2)),
|
||||||
"iamt": 0.0,
|
"iamt": 0.0,
|
||||||
"camt": 0.0,
|
"camt": 0.0,
|
||||||
|
@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
this.frm = this.get_new_frm(this.frm);
|
this.frm = this.get_new_frm(this.frm);
|
||||||
this.frm.doc.items = [];
|
this.frm.doc.items = [];
|
||||||
const res = await frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||||
args: {
|
args: {
|
||||||
'source_name': doc.name,
|
'source_name': doc.name,
|
||||||
'target_doc': this.frm.doc
|
'target_doc': this.frm.doc
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
frappe.model.sync(r.message);
|
||||||
|
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
|
||||||
|
this.set_pos_profile_data().then(() => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
frappe.model.sync(res.message);
|
|
||||||
await this.set_pos_profile_data();
|
|
||||||
frappe.dom.unfreeze();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_pos_profile_data() {
|
set_pos_profile_data() {
|
||||||
|
@ -238,4 +238,5 @@ def get_chart_data(data):
|
|||||||
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
|
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
|
||||||
},
|
},
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
|
|||||||
},
|
},
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -415,3 +415,8 @@ class Analytics(object):
|
|||||||
else:
|
else:
|
||||||
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
||||||
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
|
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
|
||||||
|
|
||||||
|
if self.filters["value_quantity"] == "Value":
|
||||||
|
self.chart["fieldtype"] = "Currency"
|
||||||
|
else:
|
||||||
|
self.chart["fieldtype"] = "Float"
|
||||||
|
@ -51,4 +51,5 @@ def get_chart_data(data, conditions, filters):
|
|||||||
},
|
},
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
@ -64,13 +64,18 @@ class Bin(Document):
|
|||||||
se = frappe.qb.DocType("Stock Entry")
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
se_item = frappe.qb.DocType("Stock Entry Detail")
|
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 = (
|
materials_transferred = (
|
||||||
frappe.qb.from_(se)
|
frappe.qb.from_(se)
|
||||||
.from_(se_item)
|
.from_(se_item)
|
||||||
.from_(po)
|
.from_(po)
|
||||||
.select(
|
.select(Sum(qty_field))
|
||||||
Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty))
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(se.docstatus == 1)
|
(se.docstatus == 1)
|
||||||
& (se.purpose == "Send to Subcontractor")
|
& (se.purpose == "Send to Subcontractor")
|
||||||
|
@ -962,6 +962,44 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
automatically_fetch_payment_terms(enable=0)
|
automatically_fetch_payment_terms(enable=0)
|
||||||
|
|
||||||
|
def test_returned_qty_in_return_dn(self):
|
||||||
|
# SO ---> SI ---> DN
|
||||||
|
# |
|
||||||
|
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||||
|
# |
|
||||||
|
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||||
|
|
||||||
|
so = make_sales_order(qty=10)
|
||||||
|
si = make_sales_invoice(so.name)
|
||||||
|
si.insert()
|
||||||
|
si.submit()
|
||||||
|
dn = make_delivery_note(si.name)
|
||||||
|
dn.insert()
|
||||||
|
dn.submit()
|
||||||
|
self.assertEqual(dn.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn.per_billed, 100)
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||||
|
|
||||||
|
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
|
||||||
|
si1 = make_sales_invoice(dn1.name)
|
||||||
|
si1.insert()
|
||||||
|
si1.submit()
|
||||||
|
dn1.reload()
|
||||||
|
self.assertEqual(dn1.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn1.per_billed, 100)
|
||||||
|
|
||||||
|
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
|
||||||
|
si2 = make_sales_invoice(dn2.name)
|
||||||
|
si2.insert()
|
||||||
|
si2.submit()
|
||||||
|
dn2.reload()
|
||||||
|
self.assertEqual(dn2.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn2.per_billed, 100)
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
@ -737,7 +737,9 @@
|
|||||||
"depends_on": "returned_qty",
|
"depends_on": "returned_qty",
|
||||||
"fieldname": "returned_qty",
|
"fieldname": "returned_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Returned Qty in Stock UOM"
|
"label": "Returned Qty in Stock UOM",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "incoming_rate",
|
"fieldname": "incoming_rate",
|
||||||
@ -778,7 +780,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-31 18:36:24.671913",
|
"modified": "2022-05-02 12:09:39.610075",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
@ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m
|
|||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||||
|
EmptyStockReconciliationItemsError,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
@ -180,9 +183,12 @@ def make_items():
|
|||||||
if not frappe.db.exists("Item", item_code):
|
if not frappe.db.exists("Item", item_code):
|
||||||
create_item(item_code)
|
create_item(item_code)
|
||||||
|
|
||||||
create_stock_reconciliation(
|
try:
|
||||||
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
|
create_stock_reconciliation(
|
||||||
)
|
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
|
||||||
|
)
|
||||||
|
except EmptyStockReconciliationItemsError:
|
||||||
|
pass
|
||||||
|
|
||||||
if frappe.db.exists("Item", "Test FG A RW 1"):
|
if frappe.db.exists("Item", "Test FG A RW 1"):
|
||||||
doc = frappe.get_doc("Item", "Test FG A RW 1")
|
doc = frappe.get_doc("Item", "Test FG A RW 1")
|
||||||
|
@ -597,7 +597,7 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
if source.material_request_type == "Customer Provided":
|
if source.material_request_type == "Customer Provided":
|
||||||
target.purpose = "Material Receipt"
|
target.purpose = "Material Receipt"
|
||||||
|
|
||||||
target.run_method("calculate_rate_and_amount")
|
target.set_missing_values()
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
target.set_job_card_data()
|
target.set_job_card_data()
|
||||||
|
|
||||||
|
@ -158,6 +158,19 @@ frappe.ui.form.on('Pick List', {
|
|||||||
get_query_filters: get_query_filters
|
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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@
|
|||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
"get_item_locations",
|
"get_item_locations",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
|
"scan_barcode",
|
||||||
|
"column_break_13",
|
||||||
|
"scan_mode",
|
||||||
|
"prompt_qty",
|
||||||
|
"section_break_15",
|
||||||
"locations",
|
"locations",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"print_settings_section",
|
"print_settings_section",
|
||||||
@ -36,6 +41,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.docstatus",
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
@ -126,11 +132,38 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Group Same Items",
|
"label": "Group Same Items",
|
||||||
"print_hide": 1
|
"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,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-21 07:56:40.646473",
|
"modified": "2022-05-11 09:09:53.029312",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
|
@ -41,8 +41,15 @@ class PickList(Document):
|
|||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
update_sales_orders = set()
|
update_sales_orders = set()
|
||||||
for item in self.locations:
|
for item in self.locations:
|
||||||
# if the user has not entered any picked qty, set it to stock_qty, before submit
|
if self.scan_mode and item.picked_qty < item.stock_qty:
|
||||||
if item.picked_qty == 0:
|
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
|
item.picked_qty = item.stock_qty
|
||||||
|
|
||||||
if item.sales_order_item:
|
if item.sales_order_item:
|
||||||
@ -672,8 +679,7 @@ def create_stock_entry(pick_list):
|
|||||||
else:
|
else:
|
||||||
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
||||||
|
|
||||||
stock_entry.set_actual_qty()
|
stock_entry.set_missing_values()
|
||||||
stock_entry.calculate_rate_and_amount()
|
|
||||||
|
|
||||||
return stock_entry.as_dict()
|
return stock_entry.as_dict()
|
||||||
|
|
||||||
|
@ -202,4 +202,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -1036,6 +1036,7 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.stock_entry_type = "Material Transfer"
|
target.stock_entry_type = "Material Transfer"
|
||||||
target.purpose = "Material Transfer"
|
target.purpose = "Material Transfer"
|
||||||
|
target.set_missing_values()
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Purchase Receipt",
|
"Purchase Receipt",
|
||||||
|
@ -470,7 +470,9 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (!r.exc) {
|
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);
|
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) {
|
function check_should_not_attach_bom_items(bom_no) {
|
||||||
return (
|
return (
|
||||||
bom_no === undefined ||
|
bom_no === undefined ||
|
||||||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
|
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2197,6 +2197,12 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
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()
|
@frappe.whitelist()
|
||||||
def move_sample_to_retention_warehouse(company, items):
|
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 make_stock_in_entry(source_name, target_doc=None):
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
|
target.set_missing_values()
|
||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
target_doc.t_warehouse = ""
|
target_doc.t_warehouse = ""
|
||||||
|
@ -132,6 +132,7 @@ def make_stock_entry(**args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
s.set_stock_entry_type()
|
s.set_stock_entry_type()
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
s.insert()
|
s.insert()
|
||||||
if not args.do_not_submit:
|
if not args.do_not_submit:
|
||||||
|
@ -652,6 +652,104 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||||
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
|
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
|
||||||
|
|
||||||
|
def test_serial_batch_item_stock_entry(self):
|
||||||
|
"""
|
||||||
|
Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
|
||||||
|
2) Cancel same Stock Entry
|
||||||
|
Expected Result: 1) Batch is created with Reference in Serial No
|
||||||
|
2) Batch is deleted and Serial No is Inactive
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
|
|
||||||
|
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
if not item:
|
||||||
|
item = create_item("Batched and Serialised Item")
|
||||||
|
item.has_batch_no = 1
|
||||||
|
item.create_new_batch = 1
|
||||||
|
item.has_serial_no = 1
|
||||||
|
item.batch_number_series = "B-BATCH-.##"
|
||||||
|
item.serial_no_series = "S-.####"
|
||||||
|
item.save()
|
||||||
|
else:
|
||||||
|
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||||
|
)
|
||||||
|
batch_no = se.items[0].batch_no
|
||||||
|
serial_no = get_serial_nos(se.items[0].serial_no)[0]
|
||||||
|
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||||
|
|
||||||
|
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||||
|
self.assertEqual(batch_in_serial_no, batch_no)
|
||||||
|
|
||||||
|
self.assertEqual(batch_qty, 1)
|
||||||
|
|
||||||
|
se.cancel()
|
||||||
|
|
||||||
|
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||||
|
self.assertEqual(batch_in_serial_no, None)
|
||||||
|
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
|
||||||
|
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||||
|
|
||||||
|
def test_serial_batch_item_qty_deduction(self):
|
||||||
|
"""
|
||||||
|
Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
|
||||||
|
Expected: 1) Cancelling first Stock Entry (origin transaction of created batch)
|
||||||
|
should throw a LinkExistsError
|
||||||
|
2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
|
||||||
|
and in that transaction only, Inactive.
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
|
|
||||||
|
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
if not item:
|
||||||
|
item = create_item("Batched and Serialised Item")
|
||||||
|
item.has_batch_no = 1
|
||||||
|
item.create_new_batch = 1
|
||||||
|
item.has_serial_no = 1
|
||||||
|
item.batch_number_series = "B-BATCH-.##"
|
||||||
|
item.serial_no_series = "S-.####"
|
||||||
|
item.save()
|
||||||
|
else:
|
||||||
|
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
|
||||||
|
se1 = make_stock_entry(
|
||||||
|
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||||
|
)
|
||||||
|
batch_no = se1.items[0].batch_no
|
||||||
|
serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
|
||||||
|
|
||||||
|
# Check Source (Origin) Document of Batch
|
||||||
|
self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
|
||||||
|
|
||||||
|
se2 = make_stock_entry(
|
||||||
|
item_code=item.item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=1,
|
||||||
|
basic_rate=100,
|
||||||
|
batch_no=batch_no,
|
||||||
|
)
|
||||||
|
serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
|
||||||
|
|
||||||
|
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||||
|
self.assertEqual(batch_qty, 2)
|
||||||
|
|
||||||
|
se2.cancel()
|
||||||
|
|
||||||
|
# Check decrease in Batch Qty
|
||||||
|
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||||
|
self.assertEqual(batch_qty, 1)
|
||||||
|
|
||||||
|
# Check if Serial No from Stock Entry 1 is intact
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
|
||||||
|
|
||||||
|
# Check if Serial No from Stock Entry 2 is Unlinked and Inactive
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
|
||||||
|
|
||||||
def test_warehouse_company_validation(self):
|
def test_warehouse_company_validation(self):
|
||||||
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
||||||
frappe.get_doc("User", "test2@example.com").add_roles(
|
frappe.get_doc("User", "test2@example.com").add_roles(
|
||||||
@ -1326,6 +1424,25 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, se.save)
|
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):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
backdated.cancel()
|
backdated.cancel()
|
||||||
self.assertEqual([1], ordered_qty_after_transaction())
|
self.assertEqual([1], ordered_qty_after_transaction())
|
||||||
|
|
||||||
|
def test_timestamp_clash(self):
|
||||||
|
|
||||||
|
item = make_item().name
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
reciept = make_stock_entry(
|
||||||
|
item_code=item,
|
||||||
|
to_warehouse=warehouse,
|
||||||
|
qty=100,
|
||||||
|
rate=10,
|
||||||
|
posting_date="2021-01-01",
|
||||||
|
posting_time="01:00:00",
|
||||||
|
)
|
||||||
|
|
||||||
|
consumption = make_stock_entry(
|
||||||
|
item_code=item,
|
||||||
|
from_warehouse=warehouse,
|
||||||
|
qty=50,
|
||||||
|
posting_date="2021-01-01",
|
||||||
|
posting_time="02:00:00.1234", # ms are possible when submitted without editing posting time
|
||||||
|
)
|
||||||
|
|
||||||
|
backdated_receipt = make_stock_entry(
|
||||||
|
item_code=item,
|
||||||
|
to_warehouse=warehouse,
|
||||||
|
qty=100,
|
||||||
|
posting_date="2021-01-01",
|
||||||
|
rate=10,
|
||||||
|
posting_time="02:00:00", # same posting time as consumption but ms part stripped
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
backdated_receipt.cancel()
|
||||||
|
except Exception as e:
|
||||||
|
self.fail("Double processing of qty for clashing timestamp.")
|
||||||
|
|
||||||
|
|
||||||
def create_repack_entry(**args):
|
def create_repack_entry(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -170,6 +170,7 @@
|
|||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.docstatus",
|
||||||
"fieldname": "section_break_22",
|
"fieldname": "section_break_22",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
@ -182,7 +183,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-27 08:57:47.161959",
|
"modified": "2022-05-11 09:10:26.327652",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation",
|
"name": "Stock Reconciliation",
|
||||||
|
@ -62,6 +62,7 @@ class StockReconciliation(StockController):
|
|||||||
self.make_sle_on_cancel()
|
self.make_sle_on_cancel()
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
def remove_items_with_no_change(self):
|
def remove_items_with_no_change(self):
|
||||||
"""Remove items if qty or rate is not changed"""
|
"""Remove items if qty or rate is not changed"""
|
||||||
@ -456,7 +457,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
key = (d.item_code, d.warehouse)
|
key = (d.item_code, d.warehouse)
|
||||||
if key not in merge_similar_entries:
|
if key not in merge_similar_entries:
|
||||||
d.total_amount = d.actual_qty * d.valuation_rate
|
d.total_amount = flt(d.actual_qty) * d.valuation_rate
|
||||||
merge_similar_entries[key] = d
|
merge_similar_entries[key] = d
|
||||||
elif d.serial_no:
|
elif d.serial_no:
|
||||||
data = merge_similar_entries[key]
|
data = merge_similar_entries[key]
|
||||||
|
@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.local.future_sle = {}
|
frappe.local.future_sle = {}
|
||||||
|
frappe.flags.pop("dont_execute_stock_reposts", None)
|
||||||
|
|
||||||
def test_reco_for_fifo(self):
|
def test_reco_for_fifo(self):
|
||||||
self._test_reco_sle_gle("FIFO")
|
self._test_reco_sle_gle("FIFO")
|
||||||
@ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
sr = create_stock_reconciliation(
|
sr = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
|
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1
|
||||||
)
|
)
|
||||||
sr.save()
|
sr.save()
|
||||||
sr.submit()
|
sr.submit()
|
||||||
@ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||||
stock_doc.cancel()
|
stock_doc.cancel()
|
||||||
|
|
||||||
|
def test_stock_reco_for_serial_and_batch_item(self):
|
||||||
|
item = create_item("_TestBatchSerialItemReco")
|
||||||
|
item.has_batch_no = 1
|
||||||
|
item.create_new_batch = 1
|
||||||
|
item.has_serial_no = 1
|
||||||
|
item.batch_number_series = "TBS-BATCH-.##"
|
||||||
|
item.serial_no_series = "TBS-.####"
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100)
|
||||||
|
|
||||||
|
batch_no = sr.items[0].batch_no
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos(sr.items[0].serial_no)
|
||||||
|
self.assertEqual(len(serial_nos), 1)
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
|
||||||
|
|
||||||
|
sr.cancel()
|
||||||
|
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
|
||||||
|
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||||
|
|
||||||
|
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
|
||||||
|
"""
|
||||||
|
Behaviour: 1) Create Stock Reconciliation, which will be the origin document
|
||||||
|
of a new batch having a serial no
|
||||||
|
2) Create a Stock Entry that adds a serial no to the same batch following this
|
||||||
|
Stock Reconciliation
|
||||||
|
3) Cancel Stock Entry
|
||||||
|
Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
item = create_item("_TestBatchSerialItemDependentReco")
|
||||||
|
item.has_batch_no = 1
|
||||||
|
item.create_new_batch = 1
|
||||||
|
item.has_serial_no = 1
|
||||||
|
item.batch_number_series = "TBSD-BATCH-.##"
|
||||||
|
item.serial_no_series = "TBSD-.####"
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
|
stock_reco = create_stock_reconciliation(
|
||||||
|
item_code=item.item_code, warehouse=warehouse, qty=1, rate=100
|
||||||
|
)
|
||||||
|
batch_no = stock_reco.items[0].batch_no
|
||||||
|
reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
|
||||||
|
|
||||||
|
stock_entry = make_stock_entry(
|
||||||
|
item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no
|
||||||
|
)
|
||||||
|
serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
|
||||||
|
|
||||||
|
# Check Batch qty after 2 transactions
|
||||||
|
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||||
|
self.assertEqual(batch_qty, 2)
|
||||||
|
|
||||||
|
# Cancel latest stock document
|
||||||
|
stock_entry.cancel()
|
||||||
|
|
||||||
|
# Check Batch qty after cancellation
|
||||||
|
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||||
|
self.assertEqual(batch_qty, 1)
|
||||||
|
|
||||||
|
# Check if Serial No from Stock Reconcilation is intact
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no)
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active")
|
||||||
|
|
||||||
|
# Check if Serial No from Stock Entry is Unlinked and Inactive
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
|
||||||
|
|
||||||
|
stock_reco.cancel()
|
||||||
|
|
||||||
def test_customer_provided_items(self):
|
def test_customer_provided_items(self):
|
||||||
item_code = "Stock-Reco-customer-Item-100"
|
item_code = "Stock-Reco-customer-Item-100"
|
||||||
create_item(
|
create_item(
|
||||||
@ -306,6 +385,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
Var | Doc | Qty | Balance
|
Var | Doc | Qty | Balance
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
PR5 | PR | 10 | 10 (posting date: today-4) [backdated]
|
||||||
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
|
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
|
||||||
PR1 | PR | 10 | 18 (posting date: today-3)
|
PR1 | PR | 10 | 18 (posting date: today-3)
|
||||||
PR2 | PR | 1 | 19 (posting date: today-2)
|
PR2 | PR | 1 | 19 (posting date: today-2)
|
||||||
@ -315,6 +395,14 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
item_code = make_item().name
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
frappe.flags.dont_execute_stock_reposts = True
|
||||||
|
|
||||||
|
def assertBalance(doc, qty_after_transaction):
|
||||||
|
sle_balance = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction"
|
||||||
|
)
|
||||||
|
self.assertEqual(sle_balance, qty_after_transaction)
|
||||||
|
|
||||||
pr1 = make_purchase_receipt(
|
pr1 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||||
)
|
)
|
||||||
@ -324,62 +412,37 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
pr3 = make_purchase_receipt(
|
pr3 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
|
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
|
||||||
)
|
)
|
||||||
|
assertBalance(pr1, 10)
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr3, 12)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
pr3_balance = frappe.db.get_value(
|
|
||||||
"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
self.assertEqual(pr1_balance, 10)
|
|
||||||
self.assertEqual(pr3_balance, 12)
|
|
||||||
|
|
||||||
# post backdated stock reco in between
|
# post backdated stock reco in between
|
||||||
sr4 = create_stock_reconciliation(
|
sr4 = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
|
item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
|
||||||
)
|
)
|
||||||
pr3_balance = frappe.db.get_value(
|
assertBalance(pr3, 7)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
self.assertEqual(pr3_balance, 7)
|
|
||||||
|
|
||||||
# post backdated stock reco at the start
|
# post backdated stock reco at the start
|
||||||
sr5 = create_stock_reconciliation(
|
sr5 = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
|
item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
|
||||||
)
|
)
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr1, 18)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
|
assertBalance(pr2, 19)
|
||||||
|
assertBalance(sr4, 6) # check if future stock reco is unaffected
|
||||||
|
|
||||||
|
# Make a backdated receipt and check only entries till first SR are affected
|
||||||
|
pr5 = make_purchase_receipt(
|
||||||
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5)
|
||||||
)
|
)
|
||||||
pr2_balance = frappe.db.get_value(
|
assertBalance(pr5, 10)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
|
# check if future stock reco is unaffected
|
||||||
)
|
assertBalance(sr4, 6)
|
||||||
sr4_balance = frappe.db.get_value(
|
assertBalance(sr5, 8)
|
||||||
"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
self.assertEqual(pr1_balance, 18)
|
|
||||||
self.assertEqual(pr2_balance, 19)
|
|
||||||
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
|
|
||||||
|
|
||||||
# cancel backdated stock reco and check future impact
|
# cancel backdated stock reco and check future impact
|
||||||
sr5.cancel()
|
sr5.cancel()
|
||||||
pr1_balance = frappe.db.get_value(
|
assertBalance(pr1, 10)
|
||||||
"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
|
assertBalance(pr2, 11)
|
||||||
)
|
assertBalance(sr4, 6) # check if future stock reco is unaffected
|
||||||
pr2_balance = frappe.db.get_value(
|
|
||||||
"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
sr4_balance = frappe.db.get_value(
|
|
||||||
"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
|
|
||||||
)
|
|
||||||
self.assertEqual(pr1_balance, 10)
|
|
||||||
self.assertEqual(pr2_balance, 11)
|
|
||||||
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
|
|
||||||
|
|
||||||
# teardown
|
|
||||||
sr4.cancel()
|
|
||||||
pr3.cancel()
|
|
||||||
pr2.cancel()
|
|
||||||
pr1.cancel()
|
|
||||||
|
|
||||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
def test_backdated_stock_reco_future_negative_stock(self):
|
def test_backdated_stock_reco_future_negative_stock(self):
|
||||||
@ -485,7 +548,6 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
|
|
||||||
# repost will make this test useless, qty should update in realtime without reposts
|
# repost will make this test useless, qty should update in realtime without reposts
|
||||||
frappe.flags.dont_execute_stock_reposts = True
|
frappe.flags.dont_execute_stock_reposts = True
|
||||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
|
||||||
|
|
||||||
item_code = make_item().name
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
@ -684,11 +746,13 @@ def create_stock_reconciliation(**args):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if not args.do_not_save:
|
||||||
if not args.do_not_submit:
|
sr.insert()
|
||||||
sr.submit()
|
try:
|
||||||
except EmptyStockReconciliationItemsError:
|
if not args.do_not_submit:
|
||||||
pass
|
sr.submit()
|
||||||
|
except EmptyStockReconciliationItemsError:
|
||||||
|
pass
|
||||||
return sr
|
return sr
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
const DIFFERNCE_FIELD_NAMES = [
|
||||||
|
"fifo_qty_diff",
|
||||||
|
"fifo_value_diff",
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item",
|
||||||
|
"options": "Item",
|
||||||
|
get_query: function() {
|
||||||
|
return {
|
||||||
|
filters: {is_stock_item: 1, has_serial_no: 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Group",
|
||||||
|
"options": "Item Group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Posting Date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Posting Date",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatter (value, row, column, data, default_formatter) {
|
||||||
|
value = default_formatter(value, row, column, data);
|
||||||
|
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
|
||||||
|
value = "<span style='color:red'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2022-05-11 04:09:13.460652",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letter_head": "abc",
|
||||||
|
"modified": "2022-05-11 04:09:20.232177",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
|
"name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Stock Ledger Entry",
|
||||||
|
"report_name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Administrator"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,212 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
|
SLE_FIELDS = (
|
||||||
|
"name",
|
||||||
|
"item_code",
|
||||||
|
"warehouse",
|
||||||
|
"posting_date",
|
||||||
|
"posting_time",
|
||||||
|
"creation",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"actual_qty",
|
||||||
|
"qty_after_transaction",
|
||||||
|
"stock_queue",
|
||||||
|
"batch_no",
|
||||||
|
"stock_value",
|
||||||
|
"valuation_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(filters):
|
||||||
|
if not any([filters.warehouse, filters.item_code, filters.item_group]):
|
||||||
|
frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
|
||||||
|
sles = get_stock_ledger_entries(filters)
|
||||||
|
return find_first_bad_queue(sles)
|
||||||
|
|
||||||
|
|
||||||
|
def get_stock_ledger_entries(filters):
|
||||||
|
|
||||||
|
sle_filters = {"is_cancelled": 0}
|
||||||
|
|
||||||
|
if filters.warehouse:
|
||||||
|
children = get_descendants_of("Warehouse", filters.warehouse)
|
||||||
|
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
|
||||||
|
|
||||||
|
if filters.item_code:
|
||||||
|
sle_filters["item_code"] = filters.item_code
|
||||||
|
elif filters.get("item_group"):
|
||||||
|
item_group = filters.get("item_group")
|
||||||
|
children = get_descendants_of("Item Group", item_group)
|
||||||
|
item_group_filter = {"item_group": ("in", children + [item_group])}
|
||||||
|
sle_filters["item_code"] = (
|
||||||
|
"in",
|
||||||
|
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.from_date:
|
||||||
|
sle_filters["posting_date"] = (">=", filters.from_date)
|
||||||
|
if filters.to_date:
|
||||||
|
sle_filters["posting_date"] = ("<=", filters.to_date)
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=SLE_FIELDS,
|
||||||
|
filters=sle_filters,
|
||||||
|
order_by="timestamp(posting_date, posting_time), creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_first_bad_queue(sles):
|
||||||
|
item_warehouse_sles = {}
|
||||||
|
for sle in sles:
|
||||||
|
item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for _item_wh, sles in item_warehouse_sles.items():
|
||||||
|
for idx, sle in enumerate(sles):
|
||||||
|
queue = json.loads(sle.stock_queue or "[]")
|
||||||
|
|
||||||
|
sle.fifo_queue_qty = 0.0
|
||||||
|
sle.fifo_stock_value = 0.0
|
||||||
|
for qty, rate in queue:
|
||||||
|
sle.fifo_queue_qty += flt(qty)
|
||||||
|
sle.fifo_stock_value += flt(qty) * flt(rate)
|
||||||
|
|
||||||
|
sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
|
||||||
|
sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
|
||||||
|
|
||||||
|
if sle.batch_no:
|
||||||
|
sle.use_batchwise_valuation = frappe.db.get_value(
|
||||||
|
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
|
||||||
|
if idx:
|
||||||
|
data.append(sles[idx - 1])
|
||||||
|
data.append(sle)
|
||||||
|
data.append({})
|
||||||
|
break
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Stock Ledger Entry"),
|
||||||
|
"options": "Stock Ledger Entry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Item Code"),
|
||||||
|
"options": "Item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Warehouse"),
|
||||||
|
"options": "Warehouse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Posting Date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_time",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Posting Time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "creation",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Creation"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Voucher Type"),
|
||||||
|
"options": "DocType",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": _("Voucher No"),
|
||||||
|
"options": "voucher_type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Batch"),
|
||||||
|
"options": "Batch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "use_batchwise_valuation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": _("Batchwise Valuation"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "actual_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("Qty Change"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "qty_after_transaction",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(A) Qty After Transaction"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_queue",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("FIFO/LIFO Queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_queue_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(C) Total qty in queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_qty_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("A - C"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_value",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(D) Balance Stock Value"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_stock_value",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(E) Balance Stock Value in Queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_value_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("D - E"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "valuation_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(H) Valuation Rate"),
|
||||||
|
},
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
@ -148,18 +149,26 @@ def get_periodic_data(entry, filters):
|
|||||||
- Warehouse A : bal_qty/value
|
- Warehouse A : bal_qty/value
|
||||||
- Warehouse B : 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 = {}
|
periodic_data = {}
|
||||||
for d in entry:
|
for d in entry:
|
||||||
period = get_period(d.posting_date, filters)
|
period = get_period(d.posting_date, filters)
|
||||||
bal_qty = 0
|
bal_qty = 0
|
||||||
|
|
||||||
|
fill_intermediate_periods(periodic_data, d.item_code, period, expected_periods)
|
||||||
|
|
||||||
# if period against item does not exist yet, instantiate it
|
# if period against item does not exist yet, instantiate it
|
||||||
# insert existing balance dict against period, and add/subtract to 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):
|
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()
|
previous_balance = periodic_data[d.item_code]["balance"].copy()
|
||||||
periodic_data[d.item_code][period] = previous_balance
|
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(
|
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get(
|
||||||
d.warehouse
|
d.warehouse
|
||||||
):
|
):
|
||||||
@ -186,6 +195,36 @@ def get_periodic_data(entry, filters):
|
|||||||
return periodic_data
|
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):
|
def get_data(filters):
|
||||||
data = []
|
data = []
|
||||||
items = get_items(filters)
|
items = get_items(filters)
|
||||||
@ -194,6 +233,8 @@ def get_data(filters):
|
|||||||
periodic_data = get_periodic_data(sle, filters)
|
periodic_data = get_periodic_data(sle, filters)
|
||||||
ranges = get_period_date_ranges(filters)
|
ranges = get_period_date_ranges(filters)
|
||||||
|
|
||||||
|
today = getdate()
|
||||||
|
|
||||||
for dummy, item_data in item_details.items():
|
for dummy, item_data in item_details.items():
|
||||||
row = {
|
row = {
|
||||||
"name": item_data.name,
|
"name": item_data.name,
|
||||||
@ -202,14 +243,15 @@ def get_data(filters):
|
|||||||
"uom": item_data.stock_uom,
|
"uom": item_data.stock_uom,
|
||||||
"brand": item_data.brand,
|
"brand": item_data.brand,
|
||||||
}
|
}
|
||||||
total = 0
|
previous_period_value = 0.0
|
||||||
for dummy, end_date in ranges:
|
for start_date, end_date in ranges:
|
||||||
period = get_period(end_date, filters)
|
period = get_period(end_date, filters)
|
||||||
period_data = periodic_data.get(item_data.name, {}).get(period)
|
period_data = periodic_data.get(item_data.name, {}).get(period)
|
||||||
amount = sum(period_data.values()) if period_data else 0
|
if period_data:
|
||||||
row[scrub(period)] = amount
|
row[scrub(period)] = previous_period_value = sum(period_data.values())
|
||||||
total += amount
|
else:
|
||||||
row["total"] = total
|
row[scrub(period)] = previous_period_value if today >= start_date else None
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -1,13 +1,59 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe import _dict
|
from frappe import _dict
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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.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):
|
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):
|
def test_get_period_date_ranges(self):
|
||||||
|
|
||||||
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
|
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)
|
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])
|
||||||
|
@ -111,17 +111,17 @@ def get_columns():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "posting_date",
|
"fieldname": "posting_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Data",
|
||||||
"label": _("Posting Date"),
|
"label": _("Posting Date"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "posting_time",
|
"fieldname": "posting_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Data",
|
||||||
"label": _("Posting Time"),
|
"label": _("Posting Time"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "creation",
|
"fieldname": "creation",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Data",
|
||||||
"label": _("Creation"),
|
"label": _("Creation"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,6 +65,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
|||||||
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
||||||
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
||||||
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
||||||
|
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
|
||||||
|
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
|
||||||
]
|
]
|
||||||
|
|
||||||
OPTIONAL_FILTERS = {
|
OPTIONAL_FILTERS = {
|
||||||
|
@ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
datetime_limit_condition = ""
|
datetime_limit_condition = ""
|
||||||
qty_shift = args.actual_qty
|
qty_shift = args.actual_qty
|
||||||
|
|
||||||
|
args["time_format"] = "%H:%i:%s"
|
||||||
|
|
||||||
# find difference/shift in qty caused by stock reconciliation
|
# find difference/shift in qty caused by stock reconciliation
|
||||||
if args.voucher_type == "Stock Reconciliation":
|
if args.voucher_type == "Stock Reconciliation":
|
||||||
qty_shift = get_stock_reco_qty_shift(args)
|
qty_shift = get_stock_reco_qty_shift(args)
|
||||||
@ -1315,7 +1317,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||||
|
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""
|
f"""
|
||||||
update `tabStock Ledger Entry`
|
update `tabStock Ledger Entry`
|
||||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||||
where
|
where
|
||||||
@ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and voucher_no != %(voucher_no)s
|
and voucher_no != %(voucher_no)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
|
and timestamp(posting_date, time_format(posting_time, %(time_format)s))
|
||||||
or (
|
> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||||
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
|
|
||||||
and creation > %(creation)s
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{datetime_limit_condition}
|
{datetime_limit_condition}
|
||||||
""".format(
|
""",
|
||||||
qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
|
|
||||||
),
|
|
||||||
args,
|
args,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1383,6 +1379,7 @@ def get_next_stock_reco(args):
|
|||||||
and creation > %(creation)s
|
and creation > %(creation)s
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
order by timestamp(posting_date, posting_time) asc, creation asc
|
||||||
limit 1
|
limit 1
|
||||||
""",
|
""",
|
||||||
args,
|
args,
|
||||||
|
@ -40,3 +40,8 @@ class TestInit(unittest.TestCase):
|
|||||||
enc_name == expected_names[i],
|
enc_name == expected_names[i],
|
||||||
"{enc} is not same as {exp}".format(enc=enc_name, exp=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")
|
||||||
|
@ -360,7 +360,7 @@ Bank Statement,Kontoauszug,
|
|||||||
Bank Statement Settings,Kontoauszug Einstellungen,
|
Bank Statement Settings,Kontoauszug Einstellungen,
|
||||||
Bank Statement balance as per General Ledger,Kontoauszug Bilanz nach Hauptbuch,
|
Bank Statement balance as per General Ledger,Kontoauszug Bilanz nach Hauptbuch,
|
||||||
Bank account cannot be named as {0},Bankname {0} ungültig,
|
Bank account cannot be named as {0},Bankname {0} ungültig,
|
||||||
Bank/Cash transactions against party or for internal transfer,Bank / Geldgeschäfte gegen Partei oder für die interne Übertragung,
|
Bank/Cash transactions against party or for internal transfer,Bank-/Bargeldtransaktionen mit einer Partei oder intern,
|
||||||
Banking,Bankwesen,
|
Banking,Bankwesen,
|
||||||
Banking and Payments,Bank- und Zahlungsverkehr,
|
Banking and Payments,Bank- und Zahlungsverkehr,
|
||||||
Barcode {0} already used in Item {1},Barcode {0} wird bereits für Artikel {1} verwendet,
|
Barcode {0} already used in Item {1},Barcode {0} wird bereits für Artikel {1} verwendet,
|
||||||
@ -1098,7 +1098,7 @@ From Datetime,Von Datum und Uhrzeit,
|
|||||||
From Delivery Note,Von Lieferschein,
|
From Delivery Note,Von Lieferschein,
|
||||||
From Fiscal Year,Ab dem Geschäftsjahr,
|
From Fiscal Year,Ab dem Geschäftsjahr,
|
||||||
From GSTIN,Von GSTIN,
|
From GSTIN,Von GSTIN,
|
||||||
From Party Name,Von Party Name,
|
From Party Name,Name des Absenders,
|
||||||
From Pin Code,Von Pin-Code,
|
From Pin Code,Von Pin-Code,
|
||||||
From Place,Von Ort,
|
From Place,Von Ort,
|
||||||
From Range has to be less than To Range,Von-Bereich muss kleiner sein als Bis-Bereich,
|
From Range has to be less than To Range,Von-Bereich muss kleiner sein als Bis-Bereich,
|
||||||
@ -1174,7 +1174,7 @@ Gross Profit / Loss,Bruttogewinn / Verlust,
|
|||||||
Gross Purchase Amount,Bruttokaufbetrag,
|
Gross Purchase Amount,Bruttokaufbetrag,
|
||||||
Gross Purchase Amount is mandatory,Bruttokaufbetrag ist erforderlich,
|
Gross Purchase Amount is mandatory,Bruttokaufbetrag ist erforderlich,
|
||||||
Group by Account,Gruppieren nach Konto,
|
Group by Account,Gruppieren nach Konto,
|
||||||
Group by Party,Gruppieren nach Parteien,
|
Group by Party,Gruppieren nach Partei,
|
||||||
Group by Voucher,Gruppieren nach Beleg,
|
Group by Voucher,Gruppieren nach Beleg,
|
||||||
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
|
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
|
||||||
Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt,
|
Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt,
|
||||||
@ -1873,12 +1873,12 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme,
|
|||||||
Part-time,Teilzeit,
|
Part-time,Teilzeit,
|
||||||
Partially Depreciated,Teilweise abgeschrieben,
|
Partially Depreciated,Teilweise abgeschrieben,
|
||||||
Partially Received,Teilweise erhalten,
|
Partially Received,Teilweise erhalten,
|
||||||
Party,Gruppe,
|
Party,Partei,
|
||||||
Party Name,Name,
|
Party Name,Name der Partei,
|
||||||
Party Type,Gruppen-Typ,
|
Party Type,Partei-Typ,
|
||||||
Party Type and Party is mandatory for {0} account,Party Type und Party ist für das Konto {0} obligatorisch,
|
Party Type and Party is mandatory for {0} account,Partei-Typ und Partei sind Pflichtfelder für Konto {0},
|
||||||
Party Type is mandatory,Party-Typ ist Pflicht,
|
Party Type is mandatory,Partei-Typ ist ein Pflichtfeld,
|
||||||
Party is mandatory,Partei ist obligatorisch,
|
Party is mandatory,Partei ist ein Pflichtfeld,
|
||||||
Password,Passwort,
|
Password,Passwort,
|
||||||
Password policy for Salary Slips is not set,Die Kennwortrichtlinie für Gehaltsabrechnungen ist nicht festgelegt,
|
Password policy for Salary Slips is not set,Die Kennwortrichtlinie für Gehaltsabrechnungen ist nicht festgelegt,
|
||||||
Past Due Date,Fälligkeitsdatum,
|
Past Due Date,Fälligkeitsdatum,
|
||||||
@ -2039,10 +2039,10 @@ Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie
|
|||||||
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
|
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
|
||||||
"Please select Item where ""Is Stock Item"" is ""No"" and ""Is Sales Item"" is ""Yes"" and there is no other Product Bundle","Bitte einen Artikel auswählen, bei dem ""Ist Lagerartikel"" mit ""Nein"" und ""Ist Verkaufsartikel"" mit ""Ja"" bezeichnet ist, und es kein anderes Produkt-Bundle gibt",
|
"Please select Item where ""Is Stock Item"" is ""No"" and ""Is Sales Item"" is ""Yes"" and there is no other Product Bundle","Bitte einen Artikel auswählen, bei dem ""Ist Lagerartikel"" mit ""Nein"" und ""Ist Verkaufsartikel"" mit ""Ja"" bezeichnet ist, und es kein anderes Produkt-Bundle gibt",
|
||||||
Please select Maintenance Status as Completed or remove Completion Date,Bitte wählen Sie Wartungsstatus als erledigt oder entfernen Sie das Abschlussdatum,
|
Please select Maintenance Status as Completed or remove Completion Date,Bitte wählen Sie Wartungsstatus als erledigt oder entfernen Sie das Abschlussdatum,
|
||||||
Please select Party Type first,Bitte zuerst Gruppentyp auswählen,
|
Please select Party Type first,Bitte zuerst Partei-Typ auswählen,
|
||||||
Please select Patient,Bitte einen Patienten auswählen,
|
Please select Patient,Bitte einen Patienten auswählen,
|
||||||
Please select Patient to get Lab Tests,"Bitte wählen Sie Patient, um Labortests zu erhalten",
|
Please select Patient to get Lab Tests,"Bitte wählen Sie Patient, um Labortests zu erhalten",
|
||||||
Please select Posting Date before selecting Party,Bitte wählen Sie Buchungsdatum vor dem Party-Auswahl,
|
Please select Posting Date before selecting Party,Bitte erst Buchungsdatum und dann die Partei auswählen,
|
||||||
Please select Posting Date first,Bitte zuerst ein Buchungsdatum auswählen,
|
Please select Posting Date first,Bitte zuerst ein Buchungsdatum auswählen,
|
||||||
Please select Price List,Bitte eine Preisliste auswählen,
|
Please select Price List,Bitte eine Preisliste auswählen,
|
||||||
Please select Program,Bitte wählen Sie Programm,
|
Please select Program,Bitte wählen Sie Programm,
|
||||||
@ -2184,7 +2184,7 @@ Process Day Book Data,Tagesbuchdaten verarbeiten,
|
|||||||
Process Master Data,Stammdaten bearbeiten,
|
Process Master Data,Stammdaten bearbeiten,
|
||||||
Processing Chart of Accounts and Parties,Verarbeiten des Kontenplans und der Parteien,
|
Processing Chart of Accounts and Parties,Verarbeiten des Kontenplans und der Parteien,
|
||||||
Processing Items and UOMs,Verarbeiten von Artikeln und Mengeneinheiten,
|
Processing Items and UOMs,Verarbeiten von Artikeln und Mengeneinheiten,
|
||||||
Processing Party Addresses,Bearbeiteradressen,
|
Processing Party Addresses,Verarbeitung der Adressen der Parteien,
|
||||||
Processing Vouchers,Bearbeitung von Gutscheinen,
|
Processing Vouchers,Bearbeitung von Gutscheinen,
|
||||||
Procurement,Beschaffung,
|
Procurement,Beschaffung,
|
||||||
Produced Qty,Produzierte Menge,
|
Produced Qty,Produzierte Menge,
|
||||||
@ -2488,8 +2488,8 @@ Row {0}: From Time and To Time of {1} is overlapping with {2},Zeile {0}: Zeitüb
|
|||||||
Row {0}: From time must be less than to time,Zeile {0}: Von Zeit zu Zeit muss kleiner sein,
|
Row {0}: From time must be less than to time,Zeile {0}: Von Zeit zu Zeit muss kleiner sein,
|
||||||
Row {0}: Hours value must be greater than zero.,Row {0}: Stunden-Wert muss größer als Null sein.,
|
Row {0}: Hours value must be greater than zero.,Row {0}: Stunden-Wert muss größer als Null sein.,
|
||||||
Row {0}: Invalid reference {1},Zeile {0}: Ungültige Referenz {1},
|
Row {0}: Invalid reference {1},Zeile {0}: Ungültige Referenz {1},
|
||||||
Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Zeile {0}: Gruppe / Konto stimmt nicht mit {1} / {2} in {3} {4} überein,
|
Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Zeile {0}: Partei / Konto stimmt nicht mit {1} / {2} in {3} {4} überein,
|
||||||
Row {0}: Party Type and Party is required for Receivable / Payable account {1},Zeile {0}: Gruppen-Typ und Gruppe sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
|
Row {0}: Party Type and Party is required for Receivable / Payable account {1},Zeile {0}: Partei-Typ und Partei sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
|
||||||
Row {0}: Payment against Sales/Purchase Order should always be marked as advance,"Zeile {0}: ""Zahlung zu Auftrag bzw. Bestellung"" sollte immer als ""Vorkasse"" eingestellt werden",
|
Row {0}: Payment against Sales/Purchase Order should always be marked as advance,"Zeile {0}: ""Zahlung zu Auftrag bzw. Bestellung"" sollte immer als ""Vorkasse"" eingestellt werden",
|
||||||
Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,"Zeile {0}: Wenn es sich um eine Vorkasse-Buchung handelt, bitte ""Ist Vorkasse"" zu Konto {1} anklicken, .",
|
Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,"Zeile {0}: Wenn es sich um eine Vorkasse-Buchung handelt, bitte ""Ist Vorkasse"" zu Konto {1} anklicken, .",
|
||||||
Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Zeile {0}: Bitte setzen Sie den Steuerbefreiungsgrund in den Umsatzsteuern und -gebühren,
|
Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Zeile {0}: Bitte setzen Sie den Steuerbefreiungsgrund in den Umsatzsteuern und -gebühren,
|
||||||
@ -3047,7 +3047,7 @@ To Deliver,Auszuliefern,
|
|||||||
To Deliver and Bill,Auszuliefern und Abzurechnen,
|
To Deliver and Bill,Auszuliefern und Abzurechnen,
|
||||||
To Fiscal Year,Bis zum Geschäftsjahr,
|
To Fiscal Year,Bis zum Geschäftsjahr,
|
||||||
To GSTIN,Zu GSTIN,
|
To GSTIN,Zu GSTIN,
|
||||||
To Party Name,Zum Party-Namen,
|
To Party Name,Name des Empfängers,
|
||||||
To Pin Code,PIN-Code,
|
To Pin Code,PIN-Code,
|
||||||
To Place,Hinstellen,
|
To Place,Hinstellen,
|
||||||
To Receive,Zu empfangen,
|
To Receive,Zu empfangen,
|
||||||
@ -3058,7 +3058,7 @@ To create a Payment Request reference document is required,Zur Erstellung eines
|
|||||||
To date can not be equal or less than from date,Bis heute kann nicht gleich oder weniger als von Datum sein,
|
To date can not be equal or less than from date,Bis heute kann nicht gleich oder weniger als von Datum sein,
|
||||||
To date can not be less than from date,Bis heute kann nicht weniger als von Datum sein,
|
To date can not be less than from date,Bis heute kann nicht weniger als von Datum sein,
|
||||||
To date can not greater than employee's relieving date,Bis heute kann nicht mehr als Entlastungsdatum des Mitarbeiters sein,
|
To date can not greater than employee's relieving date,Bis heute kann nicht mehr als Entlastungsdatum des Mitarbeiters sein,
|
||||||
"To filter based on Party, select Party Type first","Um auf der Grundlage von Gruppen zu filtern, bitte zuerst den Gruppentyp wählen",
|
"To filter based on Party, select Party Type first","Bitte Partei-Typ wählen um nach Partei zu filtern",
|
||||||
"To get the best out of ERPNext, we recommend that you take some time and watch these help videos.","Um ERPNext bestmöglich zu nutzen, empfehlen wir Ihnen, sich die Zeit zu nehmen diese Hilfevideos anzusehen.",
|
"To get the best out of ERPNext, we recommend that you take some time and watch these help videos.","Um ERPNext bestmöglich zu nutzen, empfehlen wir Ihnen, sich die Zeit zu nehmen diese Hilfevideos anzusehen.",
|
||||||
"To include tax in row {0} in Item rate, taxes in rows {1} must also be included","Um Steuern im Artikelpreis in Zeile {0} einzubeziehen, müssen Steuern in den Zeilen {1} ebenfalls einbezogen sein",
|
"To include tax in row {0} in Item rate, taxes in rows {1} must also be included","Um Steuern im Artikelpreis in Zeile {0} einzubeziehen, müssen Steuern in den Zeilen {1} ebenfalls einbezogen sein",
|
||||||
To make Customer based incentive schemes.,Um Kunden basierte Anreizsysteme zu machen.,
|
To make Customer based incentive schemes.,Um Kunden basierte Anreizsysteme zu machen.,
|
||||||
@ -4550,7 +4550,7 @@ Company Account,Firmenkonto,
|
|||||||
Account Subtype,Kontosubtyp,
|
Account Subtype,Kontosubtyp,
|
||||||
Is Default Account,Ist Standardkonto,
|
Is Default Account,Ist Standardkonto,
|
||||||
Is Company Account,Ist Unternehmenskonto,
|
Is Company Account,Ist Unternehmenskonto,
|
||||||
Party Details,Party Details,
|
Party Details,Details der Partei,
|
||||||
Account Details,Kontendaten,
|
Account Details,Kontendaten,
|
||||||
IBAN,IBAN,
|
IBAN,IBAN,
|
||||||
Bank Account No,Bankkonto Nr,
|
Bank Account No,Bankkonto Nr,
|
||||||
@ -4786,9 +4786,9 @@ Payment Order,Zahlungsauftrag,
|
|||||||
Subscription Section,Abonnementbereich,
|
Subscription Section,Abonnementbereich,
|
||||||
Journal Entry Account,Journalbuchungskonto,
|
Journal Entry Account,Journalbuchungskonto,
|
||||||
Account Balance,Kontostand,
|
Account Balance,Kontostand,
|
||||||
Party Balance,Gruppen-Saldo,
|
Party Balance,Saldo der Partei,
|
||||||
Accounting Dimensions,Abrechnungsdimensionen,
|
Accounting Dimensions,Abrechnungsdimensionen,
|
||||||
If Income or Expense,Wenn Ertrag oder Aufwand,
|
If Income or Expense,Wenn Ertrag oder Aufwand,
|
||||||
Exchange Rate,Wechselkurs,
|
Exchange Rate,Wechselkurs,
|
||||||
Debit in Company Currency,Soll in Unternehmenswährung,
|
Debit in Company Currency,Soll in Unternehmenswährung,
|
||||||
Credit in Company Currency,(Gut)Haben in Unternehmenswährung,
|
Credit in Company Currency,(Gut)Haben in Unternehmenswährung,
|
||||||
@ -4829,11 +4829,11 @@ Name of the Monthly Distribution,Bezeichnung der monatsweisen Verteilung,
|
|||||||
Monthly Distribution Percentages,Prozentuale Aufteilungen der monatsweisen Verteilung,
|
Monthly Distribution Percentages,Prozentuale Aufteilungen der monatsweisen Verteilung,
|
||||||
Monthly Distribution Percentage,Prozentuale Aufteilung der monatsweisen Verteilung,
|
Monthly Distribution Percentage,Prozentuale Aufteilung der monatsweisen Verteilung,
|
||||||
Percentage Allocation,Prozentuale Aufteilung,
|
Percentage Allocation,Prozentuale Aufteilung,
|
||||||
Create Missing Party,Erstelle fehlende Partei,
|
Create Missing Party,Fehlende Partei erstellen,
|
||||||
Create missing customer or supplier.,Erstelle einen fehlenden Kunden oder Lieferanten.,
|
Create missing customer or supplier.,Erstelle einen fehlenden Kunden oder Lieferanten.,
|
||||||
Opening Invoice Creation Tool Item,Eröffnen des Rechnungserstellungswerkzeugs,
|
Opening Invoice Creation Tool Item,Eröffnen des Rechnungserstellungswerkzeugs,
|
||||||
Temporary Opening Account,Temporäres Eröffnungskonto,
|
Temporary Opening Account,Temporäres Eröffnungskonto,
|
||||||
Party Account,Gruppenkonto,
|
Party Account,Konto der Partei,
|
||||||
Type of Payment,Zahlungsart,
|
Type of Payment,Zahlungsart,
|
||||||
ACC-PAY-.YYYY.-,ACC-PAY-.JJJJ.-,
|
ACC-PAY-.YYYY.-,ACC-PAY-.JJJJ.-,
|
||||||
Receive,Empfangen,
|
Receive,Empfangen,
|
||||||
@ -4842,7 +4842,7 @@ Payment Order Status,Zahlungsauftragsstatus,
|
|||||||
Payment Ordered,Zahlung bestellt,
|
Payment Ordered,Zahlung bestellt,
|
||||||
Payment From / To,Zahlung von / an,
|
Payment From / To,Zahlung von / an,
|
||||||
Company Bank Account,Firmenkonto,
|
Company Bank Account,Firmenkonto,
|
||||||
Party Bank Account,Party-Bankkonto,
|
Party Bank Account,Bankkonto der Partei,
|
||||||
Account Paid From,Ausgangskonto,
|
Account Paid From,Ausgangskonto,
|
||||||
Account Paid To,Eingangskonto,
|
Account Paid To,Eingangskonto,
|
||||||
Paid Amount (Company Currency),Gezahlter Betrag (Unternehmenswährung),
|
Paid Amount (Company Currency),Gezahlter Betrag (Unternehmenswährung),
|
||||||
@ -4946,7 +4946,7 @@ Is Cumulative,Ist kumulativ,
|
|||||||
Coupon Code Based,Gutscheincode basiert,
|
Coupon Code Based,Gutscheincode basiert,
|
||||||
Discount on Other Item,Rabatt auf andere Artikel,
|
Discount on Other Item,Rabatt auf andere Artikel,
|
||||||
Apply Rule On Other,Regel auf andere anwenden,
|
Apply Rule On Other,Regel auf andere anwenden,
|
||||||
Party Information,Party Informationen,
|
Party Information,Informationen zur Partei,
|
||||||
Quantity and Amount,Menge und Menge,
|
Quantity and Amount,Menge und Menge,
|
||||||
Min Qty,Mindestmenge,
|
Min Qty,Mindestmenge,
|
||||||
Max Qty,Maximalmenge,
|
Max Qty,Maximalmenge,
|
||||||
@ -5048,7 +5048,7 @@ Group same items,Gruppe gleichen Artikel,
|
|||||||
Print Language,Drucksprache,
|
Print Language,Drucksprache,
|
||||||
"Once set, this invoice will be on hold till the set date","Einmal eingestellt, wird diese Rechnung bis zum festgelegten Datum gehalten",
|
"Once set, this invoice will be on hold till the set date","Einmal eingestellt, wird diese Rechnung bis zum festgelegten Datum gehalten",
|
||||||
Credit To,Gutschreiben auf,
|
Credit To,Gutschreiben auf,
|
||||||
Party Account Currency,Gruppenkonten-Währung,
|
Party Account Currency,Währung des Kontos der Partei,
|
||||||
Against Expense Account,Zu Aufwandskonto,
|
Against Expense Account,Zu Aufwandskonto,
|
||||||
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
|
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
|
||||||
Is Internal Supplier,Ist interner Lieferant,
|
Is Internal Supplier,Ist interner Lieferant,
|
||||||
@ -5063,7 +5063,7 @@ Accepted Qty,Akzeptierte Menge,
|
|||||||
Rejected Qty,Abgelehnt Menge,
|
Rejected Qty,Abgelehnt Menge,
|
||||||
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
|
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
|
||||||
Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
|
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 ,Preis,
|
||||||
Rate (Company Currency),Preis (Unternehmenswährung),
|
Rate (Company Currency),Preis (Unternehmenswährung),
|
||||||
Amount (Company Currency),Betrag (Unternehmenswährung),
|
Amount (Company Currency),Betrag (Unternehmenswährung),
|
||||||
@ -5650,7 +5650,7 @@ From Time ,Von-Zeit,
|
|||||||
Campaign Email Schedule,Kampagnen-E-Mail-Zeitplan,
|
Campaign Email Schedule,Kampagnen-E-Mail-Zeitplan,
|
||||||
Send After (days),Senden nach (Tage),
|
Send After (days),Senden nach (Tage),
|
||||||
Signed,Unterzeichnet,
|
Signed,Unterzeichnet,
|
||||||
Party User,Party Benutzer,
|
Party User,Benutzer der Partei,
|
||||||
Unsigned,Nicht unterzeichnet,
|
Unsigned,Nicht unterzeichnet,
|
||||||
Fulfilment Status,Erfüllungsstatus,
|
Fulfilment Status,Erfüllungsstatus,
|
||||||
N/A,nicht verfügbar,
|
N/A,nicht verfügbar,
|
||||||
@ -6517,20 +6517,20 @@ Driver licence class,Führerscheinklasse,
|
|||||||
HR-EMP-,HR-EMP-,
|
HR-EMP-,HR-EMP-,
|
||||||
Employment Type,Art der Beschäftigung,
|
Employment Type,Art der Beschäftigung,
|
||||||
Emergency Contact,Notfallkontakt,
|
Emergency Contact,Notfallkontakt,
|
||||||
Emergency Contact Name,Notfall Kontaktname,
|
Emergency Contact Name,Name des Notfallkontakts,
|
||||||
Emergency Phone,Notruf,
|
Emergency Phone,Telefonnummer des Notfallkontakts,
|
||||||
ERPNext User,ERPNext Benutzer,
|
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.",
|
"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,
|
Create User Permission,Benutzerberechtigung Erstellen,
|
||||||
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
|
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
|
||||||
Joining Details,Details des Beitritts,
|
Joining Details,Details des Beitritts,
|
||||||
Offer Date,Angebotsdatum,
|
Offer Date,Angebotsdatum,
|
||||||
Confirmation Date,Datum bestätigen,
|
Confirmation Date,Bestätigungsdatum,
|
||||||
Contract End Date,Vertragsende,
|
Contract End Date,Vertragsende,
|
||||||
Notice (days),Meldung(s)(-Tage),
|
Notice (days),Kündigungsfrist (Tage),
|
||||||
Date Of Retirement,Zeitpunkt der Pensionierung,
|
Date Of Retirement,Zeitpunkt der Pensionierung,
|
||||||
Department and Grade,Abteilung und Klasse,
|
Department and Grade,Abteilung und Klasse,
|
||||||
Reports to,Berichte an,
|
Reports to,Vorgesetzter,
|
||||||
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
||||||
Leave Policy,Urlaubsrichtlinie,
|
Leave Policy,Urlaubsrichtlinie,
|
||||||
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
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,
|
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
|
||||||
Current Address Is,Aktuelle Adresse ist,
|
Current Address Is,Aktuelle Adresse ist,
|
||||||
Current Address,Aktuelle Adresse,
|
Current Address,Aktuelle Adresse,
|
||||||
Personal Bio,Persönliches Bio,
|
Personal Bio,Lebenslauf,
|
||||||
Bio / Cover Letter,Bio / Anschreiben,
|
Bio / Cover Letter,Lebenslauf / Anschreiben,
|
||||||
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
|
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
|
||||||
Passport Number,Passnummer,
|
Passport Number,Passnummer,
|
||||||
Date of Issue,Ausstellungsdatum,
|
Date of Issue,Ausstellungsdatum,
|
||||||
@ -8598,7 +8598,7 @@ Territory-wise Sales,Gebietsbezogene Verkäufe,
|
|||||||
Total Stock Summary,Gesamt Stock Zusammenfassung,
|
Total Stock Summary,Gesamt Stock Zusammenfassung,
|
||||||
Trial Balance,Probebilanz,
|
Trial Balance,Probebilanz,
|
||||||
Trial Balance (Simple),Probebilanz (einfach),
|
Trial Balance (Simple),Probebilanz (einfach),
|
||||||
Trial Balance for Party,Summen- und Saldenliste für Gruppe,
|
Trial Balance for Party,Summen- und Saldenliste für Partei,
|
||||||
Unpaid Expense Claim,Ungezahlte Spesenabrechnung,
|
Unpaid Expense Claim,Ungezahlte Spesenabrechnung,
|
||||||
Warehouse wise Item Balance Age and Value,Lagerweise Item Balance Alter und Wert,
|
Warehouse wise Item Balance Age and Value,Lagerweise Item Balance Alter und Wert,
|
||||||
Work Order Stock Report,Arbeitsauftragsbericht,
|
Work Order Stock Report,Arbeitsauftragsbericht,
|
||||||
@ -9533,7 +9533,7 @@ Account {0} exists in parent company {1}.,Konto {0} existiert in der Muttergesel
|
|||||||
"To overrule this, enable '{0}' in company {1}","Um dies zu überschreiben, aktivieren Sie '{0}' in Firma {1}",
|
"To overrule this, enable '{0}' in company {1}","Um dies zu überschreiben, aktivieren Sie '{0}' in Firma {1}",
|
||||||
Invalid condition expression,Ungültiger Bedingungsausdruck,
|
Invalid condition expression,Ungültiger Bedingungsausdruck,
|
||||||
Please Select a Company First,Bitte wählen Sie zuerst eine Firma aus,
|
Please Select a Company First,Bitte wählen Sie zuerst eine Firma aus,
|
||||||
Please Select Both Company and Party Type First,Bitte wählen Sie zuerst Firmen- und Partytyp aus,
|
Please Select Both Company and Party Type First,Bitte zuerst Unternehmen und Partei-Typ auswählen,
|
||||||
Provide the invoice portion in percent,Geben Sie den Rechnungsanteil in Prozent an,
|
Provide the invoice portion in percent,Geben Sie den Rechnungsanteil in Prozent an,
|
||||||
Give number of days according to prior selection,Geben Sie die Anzahl der Tage gemäß vorheriger Auswahl an,
|
Give number of days according to prior selection,Geben Sie die Anzahl der Tage gemäß vorheriger Auswahl an,
|
||||||
Email Details,E-Mail-Details,
|
Email Details,E-Mail-Details,
|
||||||
@ -9542,7 +9542,7 @@ Preview Email,Vorschau E-Mail,
|
|||||||
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
|
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
|
||||||
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
|
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
|
||||||
"Home, Work, etc.","Zuhause, Arbeit usw.",
|
"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,
|
Condition and formula,Zustand und Formel,
|
||||||
Sets 'Target Warehouse' in each row of the Items table.,Legt 'Ziellager' in jeder Zeile der Elementtabelle fest.,
|
Sets 'Target Warehouse' in each row of the Items table.,Legt 'Ziellager' in jeder Zeile der Elementtabelle fest.,
|
||||||
Sets 'Source Warehouse' in each row of the Items table.,Legt 'Source Warehouse' in jeder Zeile der Items-Tabelle fest.,
|
Sets 'Source Warehouse' in each row of the Items table.,Legt 'Source Warehouse' 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
Loading…
x
Reference in New Issue
Block a user