Merge branch 'develop' into loan_doc_reco_queries
This commit is contained in:
commit
d68187caae
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
|
||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
||||
- For documentation issues, refer to https://github.com/frappe/erpnext_com
|
||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||
the original discussion.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
|
||||
|
||||
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
|
||||
|
||||
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
||||
1. Certified ERPNext partners: https://erpnext.com/partners
|
||||
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
|
||||
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
||||
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
return dimensions
|
||||
|
||||
|
||||
def get_dimension_with_children(doctype, dimension):
|
||||
def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
if isinstance(dimension, list):
|
||||
dimension = dimension[0]
|
||||
if isinstance(dimensions, str):
|
||||
dimensions = [dimensions]
|
||||
|
||||
all_dimensions = []
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||
)
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
for dimension in dimensions:
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||
)
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
return all_dimensions
|
||||
|
||||
|
@ -94,7 +94,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
unlink_ref_doc_from_salary_slip(self.name)
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
self.update_expense_claim()
|
||||
|
@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_expense_claim()
|
||||
self.update_outstanding_amounts()
|
||||
@ -346,6 +346,12 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||
frappe.throw(
|
||||
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
|
||||
title=_("Invalid Invoice"),
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
||||
|
||||
|
@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
||||
)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
# block invoice after creating payment entry
|
||||
# since `get_payment_entry` will not attach blocked invoice to payment
|
||||
pi.block_invoice()
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
pe.save()
|
||||
|
||||
self.assertTrue("is on hold" in str(err.exception).lower())
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Payment Ledger Entry', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
@ -0,0 +1,180 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:PLE-{YY}-{MM}-{######}",
|
||||
"creation": "2022-05-09 19:35:03.334361",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"company",
|
||||
"account_type",
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"due_date",
|
||||
"cost_center",
|
||||
"finance_book",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"account_currency",
|
||||
"amount_in_account_currency",
|
||||
"delinked"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Account Type",
|
||||
"options": "Receivable\nPayable"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Party Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Party",
|
||||
"options": "party_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Against Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Against Voucher No",
|
||||
"options": "against_voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_in_account_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount in Account Currency",
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "DeLinked"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-19 18:04:44.609115",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "voucher_no, against_voucher_no",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentLedgerEntry(Document):
|
||||
def validate_account(self):
|
||||
valid_account = frappe.db.get_list(
|
||||
"Account",
|
||||
"name",
|
||||
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
|
||||
ignore_permissions=True,
|
||||
)
|
||||
if not valid_account:
|
||||
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
@ -0,0 +1,408 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses - _PL"
|
||||
self.income_account = "Sales - _PL"
|
||||
self.expense_account = "Cost of Goods Sold - _PL"
|
||||
self.debit_to = "Debtors - _PL"
|
||||
self.creditors = "Creditors - _PL"
|
||||
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PL"):
|
||||
self.bank = "HDFC - _PL"
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - _PL",
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
def create_item(self):
|
||||
item_name = "_Test PL Item"
|
||||
item = create_item(
|
||||
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test PL Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate()):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=amount,
|
||||
)
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
je.user_remark = "test"
|
||||
if not cost_center:
|
||||
cost_center = self.cost_center
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": acc1,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount if amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": acc2,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount if amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
return je
|
||||
|
||||
def test_payment_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
ple = self.ple
|
||||
|
||||
# full payment using PE
|
||||
si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
pe1 = get_payment_entry(si1.doctype, si1.name).save().submit()
|
||||
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where((ple.against_voucher_type == si1.doctype) & (ple.against_voucher_no == si1.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
{
|
||||
"voucher_type": si1.doctype,
|
||||
"voucher_no": si1.name,
|
||||
"against_voucher_type": si1.doctype,
|
||||
"against_voucher_no": si1.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
{
|
||||
"voucher_type": pe1.doctype,
|
||||
"voucher_no": pe1.name,
|
||||
"against_voucher_type": si1.doctype,
|
||||
"against_voucher_no": si1.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
|
||||
def test_partial_payment_against_invoice(self):
|
||||
ple = self.ple
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
# partial payment of invoice using PE
|
||||
si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
pe2 = get_payment_entry(si2.doctype, si2.name)
|
||||
pe2.get("references")[0].allocated_amount = 50
|
||||
pe2.get("references")[0].outstanding_amount = 50
|
||||
pe2 = pe2.save().submit()
|
||||
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where((ple.against_voucher_type == si2.doctype) & (ple.against_voucher_no == si2.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
{
|
||||
"voucher_type": si2.doctype,
|
||||
"voucher_no": si2.name,
|
||||
"against_voucher_type": si2.doctype,
|
||||
"against_voucher_no": si2.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
{
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
"against_voucher_type": si2.doctype,
|
||||
"against_voucher_no": si2.name,
|
||||
"amount": -50,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
|
||||
def test_cr_note_against_invoice(self):
|
||||
ple = self.ple
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
# reconcile against return invoice
|
||||
si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note1 = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note1.is_return = 1
|
||||
cr_note1.return_against = si3.name
|
||||
cr_note1 = cr_note1.save().submit()
|
||||
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where((ple.against_voucher_type == si3.doctype) & (ple.against_voucher_no == si3.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
{
|
||||
"voucher_type": si3.doctype,
|
||||
"voucher_no": si3.name,
|
||||
"against_voucher_type": si3.doctype,
|
||||
"against_voucher_no": si3.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
{
|
||||
"voucher_type": cr_note1.doctype,
|
||||
"voucher_no": cr_note1.name,
|
||||
"against_voucher_type": si3.doctype,
|
||||
"against_voucher_no": si3.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
|
||||
def test_je_against_inv_and_note(self):
|
||||
ple = self.ple
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
# reconcile against return invoice using JE
|
||||
si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note2 = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note2.is_return = 1
|
||||
cr_note2 = cr_note2.save().submit()
|
||||
je1 = self.create_journal_entry(
|
||||
self.debit_to, self.debit_to, amount, posting_date=transaction_date
|
||||
)
|
||||
je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
|
||||
je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
|
||||
je1.get("accounts")[0].reference_type = cr_note2.doctype
|
||||
je1.get("accounts")[0].reference_name = cr_note2.name
|
||||
je1.get("accounts")[1].reference_type = si4.doctype
|
||||
je1.get("accounts")[1].reference_name = si4.name
|
||||
je1 = je1.save().submit()
|
||||
|
||||
pl_entries_for_invoice = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where((ple.against_voucher_type == si4.doctype) & (ple.against_voucher_no == si4.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
{
|
||||
"voucher_type": si4.doctype,
|
||||
"voucher_no": si4.name,
|
||||
"against_voucher_type": si4.doctype,
|
||||
"against_voucher_no": si4.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
{
|
||||
"voucher_type": je1.doctype,
|
||||
"voucher_no": je1.name,
|
||||
"against_voucher_type": si4.doctype,
|
||||
"against_voucher_no": si4.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries_for_invoice[0], expected_values[0])
|
||||
self.assertEqual(pl_entries_for_invoice[1], expected_values[1])
|
||||
|
||||
pl_entries_for_crnote = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
|
||||
)
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
{
|
||||
"voucher_type": cr_note2.doctype,
|
||||
"voucher_no": cr_note2.name,
|
||||
"against_voucher_type": cr_note2.doctype,
|
||||
"against_voucher_no": cr_note2.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
{
|
||||
"voucher_type": je1.doctype,
|
||||
"voucher_no": je1.name,
|
||||
"against_voucher_type": cr_note2.doctype,
|
||||
"against_voucher_no": cr_note2.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
|
||||
self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
|
@ -78,6 +78,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
rate=400,
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
)
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
@ -86,6 +88,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
rate=200,
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(submit=False)
|
||||
@ -119,14 +123,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
create_sales_invoice(
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
cost_center=cost_center,
|
||||
rate=400,
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
|
@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
pos_opening_entry(frm) {
|
||||
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
|
||||
reset_values(frm);
|
||||
frm.trigger("set_opening_amounts");
|
||||
frm.trigger("get_pos_invoices");
|
||||
frappe.run_serially([
|
||||
() => frm.trigger("set_opening_amounts"),
|
||||
() => frm.trigger("get_pos_invoices")
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
set_opening_amounts(frm) {
|
||||
frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
|
||||
return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
|
||||
.then(({ balance_details }) => {
|
||||
balance_details.forEach(detail => {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
},
|
||||
|
||||
get_pos_invoices(frm) {
|
||||
frappe.call({
|
||||
return frappe.call({
|
||||
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
|
@ -96,6 +96,7 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "Payment Ledger Entry"
|
||||
# run on cancel method of selling controller
|
||||
super(SalesInvoice, self).on_cancel()
|
||||
if not self.is_return and self.loyalty_program:
|
||||
|
@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
title="_Test Pricing Rule with Min Qty - 2",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
item.stock_qty = 1
|
||||
si.save()
|
||||
|
@ -1418,7 +1418,12 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.db.set(self, "status", "Cancelled")
|
||||
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
def update_project(self):
|
||||
|
@ -396,7 +396,12 @@ class SalesInvoice(SellingController):
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
|
||||
self.unlink_sales_invoice_from_timesheets()
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if cint(self.update_stock):
|
||||
|
@ -3140,6 +3140,39 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertTrue(si.items[0].serial_no)
|
||||
|
||||
def test_sales_invoice_with_disabled_account(self):
|
||||
try:
|
||||
account = frappe.get_doc("Account", "VAT 5% - _TC")
|
||||
account.disabled = 1
|
||||
account.save()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.taxes = []
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "VAT 5% - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "VAT @ 5.0",
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
|
||||
self.assertTrue(
|
||||
"Cannot create accounting entries against disabled accounts" in str(err.exception)
|
||||
)
|
||||
|
||||
finally:
|
||||
account.disabled = 0
|
||||
account.save()
|
||||
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
|
@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||
|
||||
|
||||
class ClosedAccountingPeriod(frappe.ValidationError):
|
||||
@ -31,8 +32,10 @@ def make_gl_entries(
|
||||
if gl_map:
|
||||
if not cancel:
|
||||
validate_accounting_period(gl_map)
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(gl_map)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@ -45,6 +48,26 @@ def make_gl_entries(
|
||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
|
||||
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
Account = frappe.qb.DocType("Account")
|
||||
|
||||
disabled_accounts = (
|
||||
frappe.qb.from_(Account)
|
||||
.where(Account.name.isin(accounts) & Account.disabled == 1)
|
||||
.select(Account.name, Account.disabled)
|
||||
).run(as_dict=True)
|
||||
|
||||
if disabled_accounts:
|
||||
account_list = "<br>"
|
||||
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
|
||||
frappe.throw(
|
||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||
title=_("Disabled Account Selected"),
|
||||
)
|
||||
|
||||
|
||||
def validate_accounting_period(gl_map):
|
||||
accounting_periods = frappe.db.sql(
|
||||
""" SELECT
|
||||
@ -458,6 +481,7 @@ def make_reverse_gl_entries(
|
||||
).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def add_party_account(party_type, party, company, account):
|
||||
doc = frappe.get_doc(party_type, party)
|
||||
account_exists = False
|
||||
for d in doc.get("accounts"):
|
||||
if d.account == account:
|
||||
account_exists = True
|
||||
|
||||
if not account_exists:
|
||||
accounts = {"company": company, "account": account}
|
||||
|
||||
doc.append("accounts", accounts)
|
||||
|
||||
doc.save()
|
||||
|
@ -507,7 +507,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
)
|
||||
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
else:
|
||||
additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
|
||||
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
|
||||
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
||||
|
||||
|
@ -275,7 +275,7 @@ def get_conditions(filters):
|
||||
)
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
else:
|
||||
conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
@ -237,7 +237,7 @@ def get_conditions(filters):
|
||||
else:
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
@ -405,7 +405,7 @@ def get_conditions(filters):
|
||||
else:
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, filters.get(dimension.fieldname)
|
||||
)
|
||||
additional_conditions += "and {0} in %({0})s".format(dimension.fieldname)
|
||||
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
|
||||
else:
|
||||
additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname)
|
||||
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
|
||||
|
||||
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
|
||||
|
||||
|
@ -7,7 +7,7 @@ from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
|
||||
@ -15,6 +15,7 @@ import erpnext
|
||||
|
||||
# imported to enable erpnext.accounts.utils.get_account_currency
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency # noqa
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report):
|
||||
if icons:
|
||||
for icon in icons:
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
if gl_entries:
|
||||
ple = None
|
||||
|
||||
# companies
|
||||
account = qb.DocType("Account")
|
||||
companies = list(set([x.company for x in gl_entries]))
|
||||
|
||||
# receivable/payable account
|
||||
accounts_with_types = (
|
||||
qb.from_(account)
|
||||
.select(account.name, account.account_type)
|
||||
.where(
|
||||
(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
receivable_or_payable_accounts = [y.name for y in accounts_with_types]
|
||||
|
||||
def get_account_type(account):
|
||||
for entry in accounts_with_types:
|
||||
if entry.name == account:
|
||||
return entry.account_type
|
||||
|
||||
dr_or_cr = 0
|
||||
account_type = None
|
||||
for gle in gl_entries:
|
||||
if gle.account in receivable_or_payable_accounts:
|
||||
account_type = get_account_type(gle.account)
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = gle.debit - gle.credit
|
||||
dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = gle.credit - gle.debit
|
||||
dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
|
||||
|
||||
if cancel:
|
||||
dr_or_cr *= -1
|
||||
dr_or_cr_account_currency *= -1
|
||||
|
||||
ple = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Ledger Entry",
|
||||
"posting_date": gle.posting_date,
|
||||
"company": gle.company,
|
||||
"account_type": account_type,
|
||||
"account": gle.account,
|
||||
"party_type": gle.party_type,
|
||||
"party": gle.party,
|
||||
"cost_center": gle.cost_center,
|
||||
"finance_book": gle.finance_book,
|
||||
"due_date": gle.due_date,
|
||||
"voucher_type": gle.voucher_type,
|
||||
"voucher_no": gle.voucher_no,
|
||||
"against_voucher_type": gle.against_voucher_type
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
"currency": gle.currency,
|
||||
"amount": dr_or_cr,
|
||||
"amount_in_account_currency": dr_or_cr_account_currency,
|
||||
"delinked": True if cancel else False,
|
||||
}
|
||||
)
|
||||
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
ple.set(dimension.fieldname, gle.get(dimension.fieldname))
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.submit()
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry):
|
||||
if pl_entry:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
qb.update(ple)
|
||||
.set(ple.delinked, True)
|
||||
.set(ple.modified, now())
|
||||
.set(ple.modified_by, frappe.session.user)
|
||||
.where(
|
||||
(ple.company == pl_entry.company)
|
||||
& (ple.account_type == pl_entry.account_type)
|
||||
& (ple.account == pl_entry.account)
|
||||
& (ple.party_type == pl_entry.party_type)
|
||||
& (ple.party == pl_entry.party)
|
||||
& (ple.voucher_type == pl_entry.voucher_type)
|
||||
& (ple.voucher_no == pl_entry.voucher_no)
|
||||
& (ple.against_voucher_type == pl_entry.against_voucher_type)
|
||||
& (ple.against_voucher_no == pl_entry.against_voucher_no)
|
||||
)
|
||||
)
|
||||
query.run()
|
||||
|
@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "Payment Ledger Entry"
|
||||
super(PurchaseOrder, self).on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
|
@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
if (frm.doc.docstatus === 1) {
|
||||
|
||||
frm.add_custom_button(__('Supplier Quotation'),
|
||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
||||
function(){ frm.trigger("make_supplier_quotation") }, __("Create"));
|
||||
|
||||
|
||||
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||
@ -87,16 +87,24 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
|
||||
},
|
||||
|
||||
make_suppplier_quotation: function(frm) {
|
||||
make_supplier_quotation: function(frm) {
|
||||
var doc = frm.doc;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Create Supplier Quotation"),
|
||||
fields: [
|
||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
||||
{ "fieldtype": "Link",
|
||||
"label": __("Supplier"),
|
||||
"fieldname": "supplier",
|
||||
"options": doc.suppliers.map(d => d.supplier),
|
||||
"options": 'Supplier',
|
||||
"reqd": 1,
|
||||
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: [
|
||||
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
primary_action_label: __("Create"),
|
||||
primary_action: (args) => {
|
||||
|
@ -32,7 +32,9 @@
|
||||
"terms",
|
||||
"printing_settings",
|
||||
"select_print_heading",
|
||||
"letter_head"
|
||||
"letter_head",
|
||||
"more_info",
|
||||
"opportunity"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -193,6 +195,23 @@
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity",
|
||||
"fieldtype": "Link",
|
||||
"label": "Opportunity",
|
||||
"options": "Opportunity",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@ -258,7 +277,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-24 17:47:49.909000",
|
||||
"modified": "2022-04-06 17:47:49.909000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
@ -327,4 +346,4 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
from erpnext.accounts.party import (
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
get_party_gle_currency,
|
||||
validate_party_frozen_disabled,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
||||
@ -148,6 +149,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
@ -167,6 +169,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
self.validate_party_account_currency()
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
@ -382,6 +385,14 @@ class AccountsController(TransactionBase):
|
||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||
|
||||
def disable_pricing_rule_on_internal_transfer(self):
|
||||
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||
self.ignore_pricing_rule = 1
|
||||
frappe.msgprint(
|
||||
_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def validate_due_date(self):
|
||||
if self.get("is_pos"):
|
||||
return
|
||||
@ -1121,11 +1132,10 @@ class AccountsController(TransactionBase):
|
||||
{
|
||||
"account": item.discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
|
||||
dr_or_cr
|
||||
+ "_in_account_currency": flt(
|
||||
dr_or_cr: flt(
|
||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||
),
|
||||
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
},
|
||||
@ -1140,11 +1150,11 @@ class AccountsController(TransactionBase):
|
||||
{
|
||||
"account": income_or_expense_account,
|
||||
"against": supplier_or_customer,
|
||||
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
|
||||
rev_dr_cr
|
||||
+ "_in_account_currency": flt(
|
||||
rev_dr_cr: flt(
|
||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||
),
|
||||
rev_dr_cr
|
||||
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
@ -1439,6 +1449,27 @@ class AccountsController(TransactionBase):
|
||||
# at quotation / sales order level and we shouldn't stop someone
|
||||
# from creating a sales invoice if sales order is already created
|
||||
|
||||
def validate_party_account_currency(self):
|
||||
if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if self.is_opening == "Yes":
|
||||
return
|
||||
|
||||
party_type, party = self.get_party()
|
||||
party_gle_currency = get_party_gle_currency(party_type, party, self.company)
|
||||
party_account = (
|
||||
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||
)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
if not party_gle_currency and (party_account_currency != self.currency):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency and document currency should be same").format(
|
||||
frappe.bold(party_account)
|
||||
)
|
||||
)
|
||||
|
||||
def delink_advance_entries(self, linked_doc_name):
|
||||
total_allocated_amount = 0
|
||||
for adv in self.advances:
|
||||
@ -1738,6 +1769,8 @@ class AccountsController(TransactionBase):
|
||||
internal_party_field = "is_internal_customer"
|
||||
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
|
||||
internal_party_field = "is_internal_supplier"
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.get(internal_party_field) and (self.represents_company == self.company):
|
||||
return True
|
||||
|
@ -307,14 +307,15 @@ class BuyingController(StockController, Subcontracting):
|
||||
if self.is_internal_transfer():
|
||||
if rate != d.rate:
|
||||
d.rate = rate
|
||||
d.discount_percentage = 0
|
||||
d.discount_amount = 0
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
|
||||
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
|
||||
supplied_items_cost = 0.0
|
||||
|
@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{account_type_condition}
|
||||
AND is_group = 0
|
||||
AND company = %(company)s
|
||||
AND disabled = %(disabled)s
|
||||
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||
AND `{searchfield}` LIKE %(txt)s
|
||||
{mcond}
|
||||
@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
dict(
|
||||
account_types=filters.get("account_type"),
|
||||
company=filters.get("company"),
|
||||
disabled=filters.get("disabled", 0),
|
||||
currency=company_currency,
|
||||
txt="%{}%".format(txt),
|
||||
offset=start,
|
||||
|
@ -447,15 +447,16 @@ class SellingController(StockController):
|
||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
d.discount_percentage = 0
|
||||
d.discount_amount = 0
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
|
||||
elif self.get("return_against"):
|
||||
# Get incoming rate of return entry from reference document
|
||||
|
@ -2,6 +2,6 @@ def get_data():
|
||||
return {
|
||||
"fieldname": "opportunity",
|
||||
"transactions": [
|
||||
{"items": ["Quotation", "Supplier Quotation"]},
|
||||
{"items": ["Quotation", "Request for Quotation", "Supplier Quotation"]},
|
||||
],
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
self.assertEqual(quotation.net_total, 20)
|
||||
self.assertEqual(len(quotation.get("items")), 1)
|
||||
|
||||
@unittest.skip("Flaky in CI")
|
||||
def test_tax_rule(self):
|
||||
self.create_tax_rule()
|
||||
self.login_as_customer()
|
||||
|
@ -321,6 +321,7 @@ doc_events = {
|
||||
"validate": [
|
||||
"erpnext.regional.india.utils.validate_document_name",
|
||||
"erpnext.regional.india.utils.update_taxable_values",
|
||||
"erpnext.regional.india.utils.validate_sez_and_export_invoices",
|
||||
],
|
||||
},
|
||||
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
|
||||
@ -486,6 +487,7 @@ communication_doctypes = ["Customer", "Supplier"]
|
||||
|
||||
accounting_dimension_doctypes = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
|
@ -32,6 +32,9 @@ class Attendance(Document):
|
||||
self.validate_employee_status()
|
||||
self.check_leave_record()
|
||||
|
||||
def on_cancel(self):
|
||||
self.unlink_attendance_from_checkins()
|
||||
|
||||
def validate_attendance_date(self):
|
||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||
|
||||
@ -127,6 +130,33 @@ class Attendance(Document):
|
||||
if not emp:
|
||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||
|
||||
def unlink_attendance_from_checkins(self):
|
||||
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||
linked_logs = (
|
||||
frappe.qb.from_(EmployeeCheckin)
|
||||
.select(EmployeeCheckin.name)
|
||||
.where(EmployeeCheckin.attendance == self.name)
|
||||
.for_update()
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if linked_logs:
|
||||
(
|
||||
frappe.qb.update(EmployeeCheckin)
|
||||
.set("attendance", "")
|
||||
.where(EmployeeCheckin.attendance == self.name)
|
||||
).run()
|
||||
|
||||
frappe.msgprint(
|
||||
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
|
||||
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
|
||||
),
|
||||
title=_("Unlinked logs"),
|
||||
indicator="blue",
|
||||
is_minimizable=True,
|
||||
wide=True,
|
||||
)
|
||||
|
||||
|
||||
def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
|
||||
attendance = frappe.qb.DocType("Attendance")
|
||||
|
@ -65,9 +65,10 @@ frappe.ui.form.on('Employee Advance', {
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1 &&
|
||||
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
|
||||
|
||||
if (
|
||||
frm.doc.docstatus === 1
|
||||
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount))
|
||||
) {
|
||||
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
|
||||
frm.add_custom_button(__("Return"), function() {
|
||||
frm.trigger('make_return_entry');
|
||||
|
@ -227,11 +227,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
|
||||
in_log = out_log = None
|
||||
if not in_log:
|
||||
in_log = log if log.log_type == "IN" else None
|
||||
if in_log and not in_time:
|
||||
in_time = in_log.time
|
||||
elif not out_log:
|
||||
out_log = log if log.log_type == "OUT" else None
|
||||
|
||||
if in_log and out_log:
|
||||
out_time = out_log.time
|
||||
total_hours += time_diff_in_hours(in_log.time, out_log.time)
|
||||
|
||||
return total_hours, in_time, out_time
|
||||
|
||||
|
||||
|
@ -76,6 +76,17 @@ class TestEmployeeCheckin(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(attendance_count, 1)
|
||||
|
||||
def test_unlink_attendance_on_cancellation(self):
|
||||
employee = make_employee("test_mark_attendance_and_link_log@example.com")
|
||||
logs = make_n_checkins(employee, 3)
|
||||
|
||||
frappe.db.delete("Attendance", {"employee": employee})
|
||||
attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2)
|
||||
attendance.cancel()
|
||||
|
||||
linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name})
|
||||
self.assertEquals(len(linked_logs), 0)
|
||||
|
||||
def test_calculate_working_hours(self):
|
||||
check_in_out_type = [
|
||||
"Alternating entries as IN and OUT during the same shift",
|
||||
@ -125,6 +136,11 @@ class TestEmployeeCheckin(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
|
||||
|
||||
working_hours = calculate_working_hours(
|
||||
[logs_type_2[1], logs_type_2[-1]], check_in_out_type[1], working_hours_calc_type[1]
|
||||
)
|
||||
self.assertEqual(working_hours, (5.0, logs_type_2[1].time, logs_type_2[-1].time))
|
||||
|
||||
def test_fetch_shift(self):
|
||||
employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
|
||||
|
||||
|
@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_task_and_project()
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
if self.payable_account:
|
||||
self.make_gl_entries(cancel=True)
|
||||
|
||||
|
@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
||||
from erpnext.hr.utils import set_employee_name, validate_active_employee
|
||||
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
||||
@ -107,7 +107,10 @@ class LeaveEncashment(Document):
|
||||
self.leave_balance = (
|
||||
allocation.total_leaves_allocated
|
||||
- allocation.carry_forwarded_leaves_count
|
||||
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
|
||||
# adding this because the function returns a -ve number
|
||||
+ get_leaves_for_period(
|
||||
self.employee, self.leave_type, allocation.from_date, self.encashment_date
|
||||
)
|
||||
)
|
||||
|
||||
encashable_days = self.leave_balance - frappe.db.get_value(
|
||||
@ -126,14 +129,25 @@ class LeaveEncashment(Document):
|
||||
return True
|
||||
|
||||
def get_leave_allocation(self):
|
||||
leave_allocation = frappe.db.sql(
|
||||
"""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
|
||||
between from_date and to_date and docstatus=1 and leave_type='{1}'
|
||||
and employee= '{2}'""".format(
|
||||
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
|
||||
),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
date = self.encashment_date or getdate()
|
||||
|
||||
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||
leave_allocation = (
|
||||
frappe.qb.from_(LeaveAllocation)
|
||||
.select(
|
||||
LeaveAllocation.name,
|
||||
LeaveAllocation.from_date,
|
||||
LeaveAllocation.to_date,
|
||||
LeaveAllocation.total_leaves_allocated,
|
||||
LeaveAllocation.carry_forwarded_leaves_count,
|
||||
)
|
||||
.where(
|
||||
((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date))
|
||||
& (LeaveAllocation.docstatus == 1)
|
||||
& (LeaveAllocation.leave_type == self.leave_type)
|
||||
& (LeaveAllocation.employee == self.employee)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return leave_allocation[0] if leave_allocation else None
|
||||
|
||||
|
@ -4,26 +4,42 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, today
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
|
||||
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
test_dependencies = ["Leave Type"]
|
||||
test_records = frappe.get_test_records("Leave Type")
|
||||
|
||||
|
||||
class TestLeaveEncashment(unittest.TestCase):
|
||||
class TestLeaveEncashment(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
||||
frappe.db.sql("""delete from `tabAdditional Salary`""")
|
||||
frappe.db.delete("Leave Period")
|
||||
frappe.db.delete("Leave Policy Assignment")
|
||||
frappe.db.delete("Leave Allocation")
|
||||
frappe.db.delete("Leave Ledger Entry")
|
||||
frappe.db.delete("Additional Salary")
|
||||
frappe.db.delete("Leave Encashment")
|
||||
|
||||
if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
|
||||
frappe.get_doc(test_records[2]).insert()
|
||||
|
||||
date = getdate()
|
||||
year_start = getdate(get_year_start(date))
|
||||
year_end = getdate(get_year_ending(date))
|
||||
|
||||
make_holiday_list("_Test Leave Encashment", year_start, year_end)
|
||||
|
||||
# create the leave policy
|
||||
leave_policy = create_leave_policy(
|
||||
@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
leave_policy.submit()
|
||||
|
||||
# create employee, salary structure and assignment
|
||||
self.employee = make_employee("test_employee_encashment@example.com")
|
||||
self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company")
|
||||
|
||||
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
|
||||
self.leave_period = create_leave_period(year_start, year_end, "_Test Company")
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
other_details={"leave_encashment_amount_per_day": 50},
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for dt in [
|
||||
"Leave Period",
|
||||
"Leave Allocation",
|
||||
"Leave Ledger Entry",
|
||||
"Additional Salary",
|
||||
"Leave Encashment",
|
||||
"Salary Structure",
|
||||
"Leave Policy",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_leave_balance_value_and_amount(self):
|
||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
payroll_date=today(),
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||
self.assertTrue(add_sal)
|
||||
|
||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_leave_balance_value_with_leaves_and_amount(self):
|
||||
date = self.leave_period.from_date
|
||||
leave_application = make_leave_application(
|
||||
self.employee, date, add_days(date, 3), "_Test Leave Type Encashment"
|
||||
)
|
||||
leave_application.reload()
|
||||
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
payroll_date=today(),
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
|
||||
self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days)
|
||||
# encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1
|
||||
# with charge of 50 per day
|
||||
self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5)
|
||||
self.assertEqual(leave_encashment.encashment_amount, 50)
|
||||
|
||||
leave_encashment.submit()
|
||||
|
||||
# assert links
|
||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||
self.assertTrue(add_sal)
|
||||
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
|
@ -4,21 +4,21 @@
|
||||
frappe.query_reports["Employee Leave Balance"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_default("year_start_date")
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"fieldname": "to_date",
|
||||
"label": __("To Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_default("year_end_date")
|
||||
},
|
||||
{
|
||||
"fieldname":"company",
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
@ -26,16 +26,29 @@ frappe.query_reports["Employee Leave Balance"] = {
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname":"department",
|
||||
"fieldname": "department",
|
||||
"label": __("Department"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Department",
|
||||
},
|
||||
{
|
||||
"fieldname":"employee",
|
||||
"fieldname": "employee",
|
||||
"label": __("Employee"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Employee",
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_status",
|
||||
"label": __("Employee Status"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
"",
|
||||
{ "value": "Active", "label": __("Active") },
|
||||
{ "value": "Inactive", "label": __("Inactive") },
|
||||
{ "value": "Suspended", "label": __("Suspended") },
|
||||
{ "value": "Left", "label": __("Left") },
|
||||
],
|
||||
"default": "Active",
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -168,9 +168,8 @@ def get_opening_balance(
|
||||
|
||||
|
||||
def get_conditions(filters: Filters) -> Dict:
|
||||
conditions = {
|
||||
"status": "Active",
|
||||
}
|
||||
conditions = {}
|
||||
|
||||
if filters.get("employee"):
|
||||
conditions["name"] = filters.get("employee")
|
||||
|
||||
@ -180,6 +179,9 @@ def get_conditions(filters: Filters) -> Dict:
|
||||
if filters.get("department"):
|
||||
conditions["department"] = filters.get("department")
|
||||
|
||||
if filters.get("employee_status"):
|
||||
conditions["status"] = filters.get("employee_status")
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
|
@ -207,3 +207,40 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
||||
allocation1.new_leaves_allocated - leave_application.total_leave_days
|
||||
)
|
||||
self.assertEqual(report[1][0].opening_balance, opening_balance)
|
||||
|
||||
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||
def test_employee_status_filter(self):
|
||||
frappe.get_doc(test_records[0]).insert()
|
||||
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
|
||||
|
||||
allocation = make_allocation_record(
|
||||
employee=inactive_emp,
|
||||
from_date=self.year_start,
|
||||
to_date=self.year_end,
|
||||
leaves=5,
|
||||
)
|
||||
|
||||
# set employee as inactive
|
||||
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": allocation.from_date,
|
||||
"to_date": allocation.to_date,
|
||||
"employee": inactive_emp,
|
||||
"employee_status": "Active",
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
self.assertEqual(len(report[1]), 0)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": allocation.from_date,
|
||||
"to_date": allocation.to_date,
|
||||
"employee": inactive_emp,
|
||||
"employee_status": "Inactive",
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
@ -30,6 +30,19 @@ frappe.query_reports['Employee Leave Balance Summary'] = {
|
||||
label: __('Department'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Department',
|
||||
},
|
||||
{
|
||||
fieldname: "employee_status",
|
||||
label: __("Employee Status"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
"",
|
||||
{ "value": "Active", "label": __("Active") },
|
||||
{ "value": "Inactive", "label": __("Inactive") },
|
||||
{ "value": "Suspended", "label": __("Suspended") },
|
||||
{ "value": "Left", "label": __("Left") },
|
||||
],
|
||||
default: "Active",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -35,9 +35,10 @@ def get_columns(leave_types):
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = {
|
||||
"status": "Active",
|
||||
"company": filters.company,
|
||||
}
|
||||
if filters.get("employee_status"):
|
||||
conditions.update({"status": filters.get("employee_status")})
|
||||
if filters.get("department"):
|
||||
conditions.update({"department": filters.get("department")})
|
||||
if filters.get("employee"):
|
||||
|
@ -36,7 +36,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
||||
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
||||
|
||||
self.date = getdate()
|
||||
@ -146,3 +145,37 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
||||
]
|
||||
|
||||
self.assertEqual(report[1], expected_data)
|
||||
|
||||
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||
def test_employee_status_filter(self):
|
||||
frappe.get_doc(test_records[0]).insert()
|
||||
|
||||
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
|
||||
allocation = make_allocation_record(
|
||||
employee=inactive_emp, from_date=self.year_start, to_date=self.year_end
|
||||
)
|
||||
|
||||
# set employee as inactive
|
||||
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"date": allocation.from_date,
|
||||
"company": "_Test Company",
|
||||
"employee": inactive_emp,
|
||||
"employee_status": "Active",
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
self.assertEqual(len(report[1]), 0)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"date": allocation.from_date,
|
||||
"company": "_Test Company",
|
||||
"employee": inactive_emp,
|
||||
"employee_status": "Inactive",
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
@ -264,6 +264,7 @@ class LoanRepayment(AccountsController):
|
||||
regenerate_repayment_schedule(self.against_loan, cancel)
|
||||
|
||||
def allocate_amounts(self, repayment_details):
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||
self.set("repayment_details", [])
|
||||
self.principal_amount_paid = 0
|
||||
self.total_penalty_paid = 0
|
||||
@ -278,9 +279,9 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
if interest_paid > 0:
|
||||
if self.penalty_amount and interest_paid > self.penalty_amount:
|
||||
self.total_penalty_paid = self.penalty_amount
|
||||
self.total_penalty_paid = flt(self.penalty_amount, precision)
|
||||
elif self.penalty_amount:
|
||||
self.total_penalty_paid = interest_paid
|
||||
self.total_penalty_paid = flt(interest_paid, precision)
|
||||
|
||||
interest_paid -= self.total_penalty_paid
|
||||
|
||||
@ -447,8 +448,6 @@ class LoanRepayment(AccountsController):
|
||||
"remarks": remarks,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
"party_type": self.applicant_type if self.repay_from_salary else "",
|
||||
"party": self.applicant if self.repay_from_salary else "",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -499,15 +499,11 @@ cur_frm.cscript.qty = function(doc) {
|
||||
|
||||
cur_frm.cscript.rate = function(doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
var scrap_items = false;
|
||||
|
||||
if(cdt == 'BOM Scrap Item') {
|
||||
scrap_items = true;
|
||||
}
|
||||
const is_scrap_item = cdt == "BOM Scrap Item";
|
||||
|
||||
if (d.bom_no) {
|
||||
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
|
||||
get_bom_material_detail(doc, cdt, cdn, scrap_items);
|
||||
get_bom_material_detail(doc, cdt, cdn, is_scrap_item);
|
||||
} else {
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||
|
@ -33,7 +33,6 @@
|
||||
"amount",
|
||||
"base_amount",
|
||||
"section_break_18",
|
||||
"scrap",
|
||||
"qty_consumed_per_unit",
|
||||
"section_break_27",
|
||||
"has_variants",
|
||||
@ -223,15 +222,6 @@
|
||||
"fieldname": "section_break_18",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "scrap",
|
||||
"fieldtype": "Float",
|
||||
"label": "Scrap %",
|
||||
"oldfieldname": "scrap",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_consumed_per_unit",
|
||||
"fieldtype": "Float",
|
||||
@ -298,7 +288,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-24 16:57:57.020232",
|
||||
"modified": "2022-05-19 02:32:43.785470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Item",
|
||||
|
@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class JobCardOverTransferError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class JobCard(Document):
|
||||
def onload(self):
|
||||
excess_transfer = frappe.db.get_single_value(
|
||||
@ -522,23 +526,50 @@ class JobCard(Document):
|
||||
},
|
||||
)
|
||||
|
||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
||||
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
def _validate_over_transfer(row, transferred_qty):
|
||||
"Block over transfer of items if not allowed in settings."
|
||||
required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty")
|
||||
is_excess = flt(transferred_qty) > flt(required_qty)
|
||||
if is_excess:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
|
||||
).format(
|
||||
row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card
|
||||
),
|
||||
title=_("Excess Transfer"),
|
||||
exc=JobCardOverTransferError,
|
||||
)
|
||||
|
||||
for row in ste_doc.items:
|
||||
if not row.job_card_item:
|
||||
continue
|
||||
|
||||
qty = frappe.db.sql(
|
||||
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
|
||||
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
|
||||
se.purpose = 'Material Transfer for Manufacture'
|
||||
""",
|
||||
(row.job_card_item),
|
||||
)[0][0]
|
||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
transferred_qty = (
|
||||
frappe.qb.from_(sed)
|
||||
.join(se)
|
||||
.on(sed.parent == se.name)
|
||||
.select(Sum(sed.qty))
|
||||
.where(
|
||||
(sed.job_card_item == row.job_card_item)
|
||||
& (se.docstatus == 1)
|
||||
& (se.purpose == "Material Transfer for Manufacture")
|
||||
)
|
||||
).run()[0][0]
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
|
||||
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||
if not allow_excess:
|
||||
_validate_over_transfer(row, transferred_qty)
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
||||
|
||||
def set_transferred_qty(self, update_status=False):
|
||||
"Set total FG Qty for which RM was transferred."
|
||||
"Set total FG Qty in Job Card for which RM was transferred."
|
||||
if not self.items:
|
||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||
|
||||
@ -866,6 +897,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
|
||||
target.set("time_logs", [])
|
||||
target.set("employee", [])
|
||||
target.set("items", [])
|
||||
target.set("sub_operations", [])
|
||||
target.set_sub_operations()
|
||||
target.get_required_items()
|
||||
target.validate_time_logs()
|
||||
|
@ -1,15 +1,25 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import random_string
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.data import add_to_date, now
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
JobCardOverTransferError,
|
||||
OperationMismatchError,
|
||||
OverlapError,
|
||||
make_corrective_job_card,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
make_stock_entry as make_stock_entry_from_jc,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@ -17,34 +27,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
class TestJobCard(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_bom_for_jc_tests()
|
||||
self.transfer_material_against: Literal["Work Order", "Job Card"] = "Work Order"
|
||||
self.source_warehouse = None
|
||||
self._work_order = None
|
||||
|
||||
transfer_material_against, source_warehouse = None, None
|
||||
@property
|
||||
def work_order(self) -> WorkOrder:
|
||||
"""Work Order lazily created for tests."""
|
||||
if not self._work_order:
|
||||
self._work_order = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=2,
|
||||
transfer_material_against=self.transfer_material_against,
|
||||
source_warehouse=self.source_warehouse,
|
||||
)
|
||||
return self._work_order
|
||||
|
||||
tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
|
||||
tests_that_transfer_against_jc = (
|
||||
"test_job_card_multiple_materials_transfer",
|
||||
"test_job_card_excess_material_transfer",
|
||||
"test_job_card_partial_material_transfer",
|
||||
)
|
||||
|
||||
if self._testMethodName in tests_that_skip_setup:
|
||||
return
|
||||
|
||||
if self._testMethodName in tests_that_transfer_against_jc:
|
||||
transfer_material_against = "Job Card"
|
||||
source_warehouse = "Stores - _TC"
|
||||
|
||||
self.work_order = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=2,
|
||||
transfer_material_against=transfer_material_against,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
def generate_required_stock(self, work_order: WorkOrder) -> None:
|
||||
"""Create twice the stock for all required items in work order."""
|
||||
for item in work_order.required_items:
|
||||
make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target=item.source_warehouse or self.source_warehouse,
|
||||
qty=item.required_qty * 2,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_job_card(self):
|
||||
def test_job_card_operations(self):
|
||||
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
|
||||
@ -58,9 +70,6 @@ class TestJobCard(FrappeTestCase):
|
||||
doc.operation_id = "Test Data"
|
||||
self.assertRaises(OperationMismatchError, doc.save)
|
||||
|
||||
for d in job_cards:
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def test_job_card_with_different_work_station(self):
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
@ -96,19 +105,11 @@ class TestJobCard(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(completed_qty, job_card.for_quantity)
|
||||
|
||||
doc.cancel()
|
||||
|
||||
for d in job_cards:
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def test_job_card_overlap(self):
|
||||
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
|
||||
jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
|
||||
|
||||
jc1 = frappe.get_doc("Job Card", jc1_name)
|
||||
jc2 = frappe.get_doc("Job Card", jc2_name)
|
||||
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
|
||||
|
||||
employee = "_T-Employee-00001" # from test records
|
||||
|
||||
@ -137,10 +138,10 @@ class TestJobCard(FrappeTestCase):
|
||||
|
||||
def test_job_card_multiple_materials_transfer(self):
|
||||
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
|
||||
)
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
@ -165,16 +166,58 @@ class TestJobCard(FrappeTestCase):
|
||||
# transfer was made for 2 fg qty in first transfer Stock Entry
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
||||
|
||||
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
||||
def test_job_card_excess_material_transfer(self):
|
||||
"Test transferring more than required RM against Job Card."
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card.name)
|
||||
transfer_entry_1.insert()
|
||||
transfer_entry_1.submit()
|
||||
|
||||
# transfer extra qty of both RM due to previously damaged RM
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||
# deliberately change 'For Quantity'
|
||||
transfer_entry_2.fg_completed_qty = 1
|
||||
transfer_entry_2.items[0].qty = 5
|
||||
transfer_entry_2.items[1].qty = 3
|
||||
transfer_entry_2.insert()
|
||||
transfer_entry_2.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||
|
||||
# Check if 'For Quantity' is negative
|
||||
# as 'transferred_qty' > Qty to Manufacture
|
||||
transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
|
||||
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
# JC is Completed with excess transfer
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
|
||||
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
|
||||
def test_job_card_excess_material_transfer_block(self):
|
||||
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||
@ -188,39 +231,19 @@ class TestJobCard(FrappeTestCase):
|
||||
transfer_entry_2.items[0].qty = 5
|
||||
transfer_entry_2.items[1].qty = 3
|
||||
transfer_entry_2.insert()
|
||||
transfer_entry_2.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||
|
||||
# Check if 'For Quantity' is negative
|
||||
# as 'transferred_qty' > Qty to Manufacture
|
||||
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
|
||||
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
# JC is Completed with excess transfer
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
|
||||
|
||||
def test_job_card_partial_material_transfer(self):
|
||||
"Test partial material transfer against Job Card"
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
||||
)
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
# partially transfer
|
||||
transfer_entry = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry = make_stock_entry_from_jc(job_card.name)
|
||||
transfer_entry.fg_completed_qty = 1
|
||||
transfer_entry.get_items()
|
||||
transfer_entry.insert()
|
||||
@ -232,7 +255,7 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(transfer_entry.items[1].qty, 3)
|
||||
|
||||
# transfer remaining
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
|
||||
self.assertEqual(transfer_entry_2.items[0].qty, 5)
|
||||
@ -277,7 +300,49 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
||||
self.assertEqual(transfer_entry.items[0].qty, 2)
|
||||
|
||||
# rollback via tearDown method
|
||||
@change_settings(
|
||||
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
|
||||
)
|
||||
def test_corrective_costing(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
self.work_order.reload()
|
||||
original_cost = self.work_order.total_operating_cost
|
||||
|
||||
# Create a corrective operation against it
|
||||
corrective_action = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.hour_rate = 100
|
||||
corrective_job_card.insert()
|
||||
corrective_job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=2),
|
||||
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
corrective_job_card.submit()
|
||||
|
||||
self.work_order.reload()
|
||||
cost_after_correction = self.work_order.total_operating_cost
|
||||
self.assertGreater(cost_after_correction, original_cost)
|
||||
|
||||
corrective_job_card.cancel()
|
||||
self.work_order.reload()
|
||||
cost_after_cancel = self.work_order.total_operating_cost
|
||||
self.assertEqual(cost_after_cancel, original_cost)
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
|
@ -21,7 +21,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
exploded_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"parent": bom},
|
||||
fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"],
|
||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
|
||||
)
|
||||
|
||||
for item in exploded_items:
|
||||
@ -37,7 +37,6 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
"qty": item.qty * qty,
|
||||
"uom": item.uom,
|
||||
"description": item.description,
|
||||
"scrap": item.scrap,
|
||||
}
|
||||
)
|
||||
if item.bom_no:
|
||||
@ -64,5 +63,4 @@ def get_columns():
|
||||
"fieldname": "description",
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100},
|
||||
]
|
||||
|
@ -313,7 +313,6 @@ erpnext.patches.v13_0.enable_uoms
|
||||
erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
||||
@ -324,7 +323,6 @@ erpnext.patches.v13_0.rename_ksa_qr_field
|
||||
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
|
||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||
erpnext.patches.v13_0.update_tax_category_for_rcm
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
erpnext.patches.v14_0.set_payroll_cost_centers
|
||||
erpnext.patches.v13_0.agriculture_deprecation_warning
|
||||
erpnext.patches.v13_0.hospitality_deprecation_warning
|
||||
@ -333,15 +331,17 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail
|
||||
erpnext.patches.v13_0.enable_provisional_accounting
|
||||
erpnext.patches.v13_0.non_profit_deprecation_warning
|
||||
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||
erpnext.patches.v14_0.delete_education_doctypes
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||
erpnext.patches.v14_0.delete_hub_doctypes
|
||||
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
|
||||
erpnext.patches.v14_0.delete_agriculture_doctypes
|
||||
erpnext.patches.v14_0.delete_education_doctypes
|
||||
erpnext.patches.v14_0.delete_datev_doctypes
|
||||
erpnext.patches.v14_0.rearrange_company_fields
|
||||
erpnext.patches.v14_0.update_leave_notification_template
|
||||
@ -359,7 +359,7 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||
erpnext.patches.v14_0.update_batch_valuation_flag
|
||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||
erpnext.patches.v14_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
@ -367,7 +367,8 @@ erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
|
||||
erpnext.patches.v13_0.requeue_recoverable_reposts
|
||||
erpnext.patches.v14_0.discount_accounting_separation
|
||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
|
21
erpnext/patches/v13_0/requeue_recoverable_reposts.py
Normal file
21
erpnext/patches/v13_0/requeue_recoverable_reposts.py
Normal file
@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
recoverable = ("QueryDeadlockError", "QueryTimeoutError", "JobTimeoutException")
|
||||
|
||||
failed_reposts = frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
fields=["name", "error_log"],
|
||||
filters={
|
||||
"status": "Failed",
|
||||
"docstatus": 1,
|
||||
"modified": (">", "2022-04-20"),
|
||||
"error_log": ("is", "set"),
|
||||
},
|
||||
)
|
||||
for riv in failed_reposts:
|
||||
for exc in recoverable:
|
||||
if exc in riv.error_log:
|
||||
frappe.db.set_value("Repost Item Valuation", riv.name, "status", "Queued")
|
||||
break
|
38
erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
Normal file
38
erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
Normal file
@ -0,0 +1,38 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimensions,
|
||||
make_dimension_in_accounting_doctypes,
|
||||
)
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||
|
||||
|
||||
def create_accounting_dimension_fields():
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
||||
|
||||
|
||||
def execute():
|
||||
# create accounting dimension fields in Payment Ledger
|
||||
create_accounting_dimension_fields()
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
accounts = frappe.db.get_list(
|
||||
"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
|
||||
)
|
||||
gl_entries = []
|
||||
if accounts:
|
||||
# get all gl entries on receivable/payable accounts
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select("*")
|
||||
.where(gl.account.isin(accounts))
|
||||
.where(gl.is_cancelled == 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if gl_entries:
|
||||
# create payment ledger entries for the accounts receivable/payable
|
||||
create_payment_ledger_entry(gl_entries, 0)
|
@ -16,6 +16,7 @@ from frappe.utils import (
|
||||
comma_and,
|
||||
date_diff,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
)
|
||||
|
||||
@ -45,6 +46,7 @@ class PayrollEntry(Document):
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_employee_details()
|
||||
self.validate_payroll_payable_account()
|
||||
if self.validate_attendance:
|
||||
if self.validate_employee_attendance():
|
||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||
@ -66,6 +68,14 @@ class PayrollEntry(Document):
|
||||
if len(emp_with_sal_slip):
|
||||
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
||||
|
||||
def validate_payroll_payable_account(self):
|
||||
if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account type cannot be set for payroll payable account {0}, please remove and try again"
|
||||
).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.delete_doc(
|
||||
"Salary Slip",
|
||||
|
@ -27,7 +27,8 @@ frappe.ui.form.on(cur_frm.doctype, {
|
||||
query: "erpnext.controllers.queries.tax_account_query",
|
||||
filters: {
|
||||
"account_type": account_type,
|
||||
"company": doc.company
|
||||
"company": doc.company,
|
||||
"disabled": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -90,7 +90,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
else {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1 }
|
||||
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -789,11 +789,23 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||
let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
||||
let base_amount, amount;
|
||||
|
||||
if (me.frm.doc.party_account_currency == me.frm.doc.currency) {
|
||||
// if customer/supplier currency is same as company currency
|
||||
// total_amount_to_pay is already in customer/supplier currency
|
||||
// so base_amount has to be calculated using total_amount_to_pay
|
||||
base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data));
|
||||
amount = flt(total_amount_to_pay, precision("amount", data));
|
||||
} else {
|
||||
base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
||||
amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
||||
}
|
||||
|
||||
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
|
||||
let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
||||
frappe.model.set_value(data.doctype, data.name, "amount", amount);
|
||||
payment_status = false;
|
||||
|
||||
} else if(me.frm.doc.paid_amount) {
|
||||
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
|
||||
}
|
||||
|
@ -923,12 +923,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
currency() {
|
||||
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */
|
||||
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
||||
/* end manqala */
|
||||
var me = this;
|
||||
// The transaction date be either transaction_date (from orders) or posting_date (from invoices)
|
||||
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
||||
|
||||
let me = this;
|
||||
this.set_dynamic_labels();
|
||||
var company_currency = this.get_company_currency();
|
||||
let company_currency = this.get_company_currency();
|
||||
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
|
||||
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
||||
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
||||
@ -942,7 +942,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.conversion_rate();
|
||||
// company currency and doc currency is same
|
||||
// this will prevent unnecessary conversion rate triggers
|
||||
this.frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ $.extend(erpnext.utils, {
|
||||
},
|
||||
|
||||
add_indicator_for_multicompany: function(frm, info) {
|
||||
frm.dashboard.stats_area.removeClass('hidden');
|
||||
frm.dashboard.stats_area.show();
|
||||
frm.dashboard.stats_area_row.addClass('flex');
|
||||
frm.dashboard.stats_area_row.css('flex-wrap', 'wrap');
|
||||
|
||||
@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
|
||||
filters.splice(index, 0, {
|
||||
"fieldname": dimension["fieldname"],
|
||||
"label": __(dimension["label"]),
|
||||
"fieldtype": "Link",
|
||||
"options": dimension["document_type"]
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options(dimension["document_type"], txt);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -31,30 +31,39 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
|
||||
process_scan() {
|
||||
let me = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
let me = this;
|
||||
|
||||
const input = this.scan_barcode_field.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const input = this.scan_barcode_field.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe
|
||||
.call({
|
||||
method: this.scan_api,
|
||||
args: {
|
||||
search_value: input,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const data = r && r.message;
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
this.show_alert(__("Cannot find Item with this Barcode"), "red");
|
||||
this.clean_up();
|
||||
return;
|
||||
}
|
||||
frappe
|
||||
.call({
|
||||
method: this.scan_api,
|
||||
args: {
|
||||
search_value: input,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const data = r && r.message;
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
this.show_alert(__("Cannot find Item with this Barcode"), "red");
|
||||
this.clean_up();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
me.update_table(data);
|
||||
});
|
||||
const row = me.update_table(data);
|
||||
if (row) {
|
||||
resolve(row);
|
||||
}
|
||||
else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
update_table(data) {
|
||||
@ -90,6 +99,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.set_batch_no(row, batch_no);
|
||||
this.set_barcode(row, barcode);
|
||||
this.clean_up();
|
||||
return row;
|
||||
}
|
||||
|
||||
// batch and serial selector is reduandant when all info can be added by scan
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:hsn_code",
|
||||
"creation": "2017-06-21 10:48:56.422086",
|
||||
"doctype": "DocType",
|
||||
@ -7,6 +8,7 @@
|
||||
"field_order": [
|
||||
"hsn_code",
|
||||
"description",
|
||||
"gst_rates",
|
||||
"taxes"
|
||||
],
|
||||
"fields": [
|
||||
@ -16,22 +18,37 @@
|
||||
"in_list_view": 1,
|
||||
"label": "HSN Code",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description"
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes",
|
||||
"fieldtype": "Table",
|
||||
"label": "Taxes",
|
||||
"options": "Item Tax"
|
||||
"options": "Item Tax",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gst_rates",
|
||||
"fieldtype": "Table",
|
||||
"label": "GST Rates",
|
||||
"options": "HSN Tax Rate",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-11-01 11:18:59.556931",
|
||||
"links": [],
|
||||
"modified": "2022-05-11 13:42:27.286643",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "GST HSN Code",
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
frappe.ui.form.on('GST Settings', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button('Send GST Update Reminder', () => {
|
||||
frm.add_custom_button(__('Send GST Update Reminder'), () => {
|
||||
return new Promise((resolve) => {
|
||||
return frappe.call({
|
||||
method: 'erpnext.regional.doctype.gst_settings.gst_settings.send_reminder'
|
||||
@ -11,6 +11,12 @@ frappe.ui.form.on('GST Settings', {
|
||||
});
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Sync HSN Codes'), () => {
|
||||
frappe.call({
|
||||
"method": "erpnext.regional.doctype.gst_settings.gst_settings.update_hsn_codes"
|
||||
});
|
||||
});
|
||||
|
||||
$(frm.fields_dict.gst_summary.wrapper).empty().html(
|
||||
`<table class="table table-bordered">
|
||||
<tbody>
|
||||
|
@ -2,13 +2,14 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, get_url, nowdate
|
||||
from frappe.utils import date_diff, flt, get_url, nowdate
|
||||
|
||||
|
||||
class EmailMissing(frappe.ValidationError):
|
||||
@ -129,3 +130,31 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
|
||||
)
|
||||
|
||||
return email_id
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_hsn_codes():
|
||||
frappe.enqueue(enqueue_update)
|
||||
frappe.msgprint(_("HSN/SAC Code sync started, this may take a few minutes..."))
|
||||
|
||||
|
||||
def enqueue_update():
|
||||
with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f:
|
||||
hsn_codes = json.loads(f.read())
|
||||
|
||||
for hsn_code in hsn_codes:
|
||||
try:
|
||||
hsn_code_doc = frappe.get_doc("GST HSN Code", hsn_code.get("hsn_code"))
|
||||
hsn_code_doc.set("gst_rates", [])
|
||||
for rate in hsn_code.get("gst_rates"):
|
||||
hsn_code_doc.append(
|
||||
"gst_rates",
|
||||
{
|
||||
"minimum_taxable_value": flt(hsn_code.get("minimum_taxable_value")),
|
||||
"tax_rate": flt(rate.get("tax_rate")),
|
||||
},
|
||||
)
|
||||
|
||||
hsn_code_doc.save()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
144352
erpnext/regional/doctype/gst_settings/hsn_code_data.json
Normal file
144352
erpnext/regional/doctype/gst_settings/hsn_code_data.json
Normal file
File diff suppressed because it is too large
Load Diff
0
erpnext/regional/doctype/hsn_tax_rate/__init__.py
Normal file
0
erpnext/regional/doctype/hsn_tax_rate/__init__.py
Normal file
39
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.json
Normal file
39
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-05-11 13:32:42.534779",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"minimum_taxable_value",
|
||||
"tax_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "minimum_taxable_value",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Minimum Taxable Value"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-15 15:37:56.152470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "HSN Tax Rate",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
9
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.py
Normal file
9
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class HSNTaxRate(Document):
|
||||
pass
|
@ -149,58 +149,27 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
}
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
const action = () => {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Cancel E-Way Bill'),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: {
|
||||
doctype,
|
||||
docname: name,
|
||||
eway_bill: ewaybill,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill Cancelled successfully'),
|
||||
indicator: 'green'
|
||||
}, 7);
|
||||
frm.reload_doc();
|
||||
d.hide();
|
||||
},
|
||||
error: () => {
|
||||
frappe.show_alert({
|
||||
message: __('E-Way Bill was not Cancelled'),
|
||||
indicator: 'red'
|
||||
}, 7);
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
|
||||
const dialog = frappe.msgprint({
|
||||
title: __('Update E-Way Bill Cancelled Status?'),
|
||||
message: message,
|
||||
indicator: 'orange',
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() && dialog.hide()
|
||||
});
|
||||
}
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
}
|
||||
|
@ -649,6 +649,8 @@ def make_einvoice(invoice):
|
||||
try:
|
||||
einvoice = safe_json_load(einvoice)
|
||||
einvoice = santize_einvoice_fields(einvoice)
|
||||
except json.JSONDecodeError:
|
||||
raise
|
||||
except Exception:
|
||||
show_link_to_error_log(invoice, einvoice)
|
||||
|
||||
@ -765,7 +767,9 @@ def safe_json_load(json_string):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Error in input data. Please check for any special characters near following input: <br> {}"
|
||||
).format(snippet)
|
||||
).format(snippet),
|
||||
title=_("Invalid JSON"),
|
||||
exc=e,
|
||||
)
|
||||
|
||||
|
||||
@ -797,7 +801,8 @@ class GSPConnector:
|
||||
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
|
||||
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
|
||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
|
||||
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
|
||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
||||
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
||||
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
||||
|
||||
@ -1185,6 +1190,7 @@ class GSPConnector:
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||
headers["username"] = headers["user_name"]
|
||||
del headers["user_name"]
|
||||
try:
|
||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||
if res.get("success"):
|
||||
@ -1358,9 +1364,13 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
def cancel_eway_bill(doctype, docname):
|
||||
# NOTE: cancel_eway_bill api is disabled by Adequare.
|
||||
# gsp_connector = GSPConnector(doctype, docname)
|
||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -22,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
|
||||
'shipping_address': frm.doc.shipping_address || '',
|
||||
'shipping_address_name': frm.doc.shipping_address_name || '',
|
||||
'customer_address': frm.doc.customer_address || '',
|
||||
'company_address': frm.doc.company_address,
|
||||
'supplier_address': frm.doc.supplier_address,
|
||||
'customer': frm.doc.customer,
|
||||
'supplier': frm.doc.supplier,
|
||||
|
@ -840,6 +840,30 @@ def get_gst_accounts(
|
||||
return gst_accounts
|
||||
|
||||
|
||||
def validate_sez_and_export_invoices(doc, method):
|
||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||
|
||||
if country != "India":
|
||||
return
|
||||
|
||||
if (
|
||||
doc.get("gst_category") in ("SEZ", "Overseas")
|
||||
and doc.get("export_type") == "Without Payment of Tax"
|
||||
):
|
||||
gst_accounts = get_gst_accounts(doc.company)
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
for tax in doc.get("taxes"):
|
||||
if (
|
||||
tax.account_head
|
||||
in gst_accounts.get("igst_account", [])
|
||||
+ gst_accounts.get("sgst_account", [])
|
||||
+ gst_accounts.get("cgst_account", [])
|
||||
and tax.tax_amount_after_discount_amount
|
||||
):
|
||||
frappe.throw(_("GST cannot be applied on SEZ or Export invoices without payment of tax"))
|
||||
|
||||
|
||||
def validate_reverse_charge_transaction(doc, method):
|
||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||
|
||||
@ -887,6 +911,8 @@ def validate_reverse_charge_transaction(doc, method):
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
doc.eligibility_for_itc = "ITC on Reverse Charge"
|
||||
|
||||
|
||||
def update_itc_availed_fields(doc, method):
|
||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||
|
@ -226,7 +226,10 @@ class Gstr1Report(object):
|
||||
taxable_value += abs(net_amount)
|
||||
elif (
|
||||
not tax_rate
|
||||
and self.filters.get("type_of_business") == "EXPORT"
|
||||
and (
|
||||
self.filters.get("type_of_business") == "EXPORT"
|
||||
or invoice_details.get("gst_category") == "SEZ"
|
||||
)
|
||||
and invoice_details.get("export_type") == "Without Payment of Tax"
|
||||
):
|
||||
taxable_value += abs(net_amount)
|
||||
@ -328,12 +331,14 @@ class Gstr1Report(object):
|
||||
def get_invoice_items(self):
|
||||
self.invoice_items = frappe._dict()
|
||||
self.item_tax_rate = frappe._dict()
|
||||
self.item_hsn_map = frappe._dict()
|
||||
self.nil_exempt_non_gst = {}
|
||||
|
||||
# nosemgrep
|
||||
items = frappe.db.sql(
|
||||
"""
|
||||
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
|
||||
is_non_gst from `tab%s Item`
|
||||
gst_hsn_code, is_non_gst from `tab%s Item`
|
||||
where parent in (%s)
|
||||
"""
|
||||
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
|
||||
@ -343,6 +348,7 @@ class Gstr1Report(object):
|
||||
|
||||
for d in items:
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
|
||||
self.item_hsn_map.setdefault(d.item_code, d.gst_hsn_code)
|
||||
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
|
||||
"base_net_amount", 0
|
||||
)
|
||||
@ -367,6 +373,8 @@ class Gstr1Report(object):
|
||||
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
|
||||
|
||||
def get_items_based_on_tax_rate(self):
|
||||
hsn_wise_tax_rate = get_hsn_wise_tax_rates()
|
||||
|
||||
self.tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
@ -427,7 +435,7 @@ class Gstr1Report(object):
|
||||
alert=True,
|
||||
)
|
||||
|
||||
# Build itemised tax for export invoices where tax table is blank
|
||||
# Build itemised tax for export invoices where tax table is blank (Export and SEZ Invoices)
|
||||
for invoice, items in self.invoice_items.items():
|
||||
if (
|
||||
invoice not in self.items_based_on_tax_rate
|
||||
@ -435,7 +443,17 @@ class Gstr1Report(object):
|
||||
and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax"
|
||||
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ")
|
||||
):
|
||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
||||
self.items_based_on_tax_rate.setdefault(invoice, {})
|
||||
for item_code in items.keys():
|
||||
hsn_code = self.item_hsn_map.get(item_code)
|
||||
tax_rate = 0
|
||||
taxable_value = items.get(item_code)
|
||||
for rates in hsn_wise_tax_rate.get(hsn_code, []):
|
||||
if taxable_value > rates.get("minimum_taxable_value"):
|
||||
tax_rate = rates.get("tax_rate")
|
||||
|
||||
self.items_based_on_tax_rate[invoice].setdefault(tax_rate, [])
|
||||
self.items_based_on_tax_rate[invoice][tax_rate].append(item_code)
|
||||
|
||||
def get_columns(self):
|
||||
self.other_columns = []
|
||||
@ -728,7 +746,7 @@ def get_json(filters, report_name, data):
|
||||
|
||||
elif filters["type_of_business"] == "EXPORT":
|
||||
for item in report_data[:-1]:
|
||||
res.setdefault(item["export_type"], []).append(item)
|
||||
res.setdefault(item["export_type"], {}).setdefault(item["invoice_number"], []).append(item)
|
||||
|
||||
out = get_export_json(res)
|
||||
gst_json["exp"] = out
|
||||
@ -918,11 +936,21 @@ def get_export_json(res):
|
||||
for exp_type in res:
|
||||
exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
|
||||
|
||||
for row in res[exp_type]:
|
||||
inv_item = get_basic_invoice_detail(row)
|
||||
inv_item["itms"] = [
|
||||
{"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0}
|
||||
]
|
||||
for number, invoice in res[exp_type].items():
|
||||
inv_item = get_basic_invoice_detail(invoice[0])
|
||||
inv_item["itms"] = []
|
||||
|
||||
for item in invoice:
|
||||
inv_item["itms"].append(
|
||||
{
|
||||
"txval": flt(item["taxable_value"], 2),
|
||||
"rt": flt(item["rate"]),
|
||||
"iamt": flt((item["taxable_value"] * flt(item["rate"])) / 100.0, 2)
|
||||
if exp_type != "WOPAY"
|
||||
else 0,
|
||||
"csamt": (flt(item.get("cess_amount"), 2) or 0),
|
||||
}
|
||||
)
|
||||
|
||||
inv.append(inv_item)
|
||||
|
||||
@ -1060,7 +1088,6 @@ def get_rate_and_tax_details(row, gstin):
|
||||
|
||||
# calculate tax amount added
|
||||
tax = flt((row["taxable_value"] * rate) / 100.0, 2)
|
||||
frappe.errprint([tax, tax / 2])
|
||||
if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
|
||||
itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
|
||||
else:
|
||||
@ -1136,3 +1163,26 @@ def get_company_gstins(company):
|
||||
address_list = [""] + [d.gstin for d in addresses]
|
||||
|
||||
return address_list
|
||||
|
||||
|
||||
def get_hsn_wise_tax_rates():
|
||||
hsn_wise_tax_rate = {}
|
||||
gst_hsn_code = frappe.qb.DocType("GST HSN Code")
|
||||
hsn_tax_rates = frappe.qb.DocType("HSN Tax Rate")
|
||||
|
||||
hsn_code_data = (
|
||||
frappe.qb.from_(gst_hsn_code)
|
||||
.inner_join(hsn_tax_rates)
|
||||
.on(gst_hsn_code.name == hsn_tax_rates.parent)
|
||||
.select(gst_hsn_code.hsn_code, hsn_tax_rates.tax_rate, hsn_tax_rates.minimum_taxable_value)
|
||||
.orderby(hsn_tax_rates.minimum_taxable_value)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
for d in hsn_code_data:
|
||||
hsn_wise_tax_rate.setdefault(d.hsn_code, [])
|
||||
hsn_wise_tax_rate[d.hsn_code].append(
|
||||
{"minimum_taxable_value": d.minimum_taxable_value, "tax_rate": d.tax_rate}
|
||||
)
|
||||
|
||||
return hsn_wise_tax_rate
|
||||
|
@ -221,7 +221,7 @@ def get_merged_data(columns, data):
|
||||
result = []
|
||||
|
||||
for row in data:
|
||||
key = row[0] + "-" + str(row[4])
|
||||
key = row[0] + "-" + row[2] + "-" + str(row[4])
|
||||
merged_hsn_dict.setdefault(key, {})
|
||||
for i, d in enumerate(columns):
|
||||
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
||||
|
@ -367,7 +367,14 @@ def set_credit_limit(customer, company, credit_limit):
|
||||
customer.credit_limits[-1].db_insert()
|
||||
|
||||
|
||||
def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
|
||||
def create_internal_customer(
|
||||
customer_name=None, represents_company=None, allowed_to_interact_with=None
|
||||
):
|
||||
if not customer_name:
|
||||
customer_name = represents_company
|
||||
if not allowed_to_interact_with:
|
||||
allowed_to_interact_with = represents_company
|
||||
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
|
@ -232,7 +232,7 @@ class SalesOrder(SellingController):
|
||||
update_coupon_code_count(self.coupon_code, "used")
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
super(SalesOrder, self).on_cancel()
|
||||
|
||||
# Cannot cancel closed SO
|
||||
|
@ -187,8 +187,9 @@ def get_so_with_invoices(filters):
|
||||
.on(soi.parent == so.name)
|
||||
.join(ps)
|
||||
.on(ps.parent == so.name)
|
||||
.select(so.name)
|
||||
.distinct()
|
||||
.select(
|
||||
so.name,
|
||||
so.customer,
|
||||
so.transaction_date.as_("submitted"),
|
||||
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
||||
|
@ -64,7 +64,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
|
||||
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer, 'has_variants': 0}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -64,13 +64,18 @@ class Bin(Document):
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
se_item = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
if frappe.db.field_exists("Stock Entry", "is_return"):
|
||||
qty_field = (
|
||||
Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
|
||||
)
|
||||
else:
|
||||
qty_field = se_item.transfer_qty
|
||||
|
||||
materials_transferred = (
|
||||
frappe.qb.from_(se)
|
||||
.from_(se_item)
|
||||
.from_(po)
|
||||
.select(
|
||||
Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty))
|
||||
)
|
||||
.select(Sum(qty_field))
|
||||
.where(
|
||||
(se.docstatus == 1)
|
||||
& (se.purpose == "Send to Subcontractor")
|
||||
|
@ -570,15 +570,12 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
customer=customer_name,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
do_not_submit=True,
|
||||
qty=5,
|
||||
rate=500,
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse=target_warehouse,
|
||||
)
|
||||
|
||||
dn.submit()
|
||||
|
||||
# qty after delivery
|
||||
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
|
||||
self.assertEqual(actual_qty_at_source, 475)
|
||||
@ -1000,6 +997,73 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn2.items[0].returned_qty, 0)
|
||||
self.assertEqual(dn2.per_billed, 100)
|
||||
|
||||
def test_internal_transfer_with_valuation_only(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
target = "Stores - _TC"
|
||||
company = "_Test Company"
|
||||
customer = create_internal_customer(represents_company=company)
|
||||
rate = 42
|
||||
|
||||
# Create item price and pricing rule
|
||||
frappe.get_doc(
|
||||
{
|
||||
"item_code": item,
|
||||
"price_list": "Standard Selling",
|
||||
"price_list_rate": 1000,
|
||||
"doctype": "Item Price",
|
||||
}
|
||||
).insert()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": frappe.generate_hash(),
|
||||
"apply_on": "Item Code",
|
||||
"price_or_product_discount": "Price",
|
||||
"selling": 1,
|
||||
"company": company,
|
||||
"margin_type": "Percentage",
|
||||
"margin_rate_or_amount": 10,
|
||||
"apply_discount_on": "Grand Total",
|
||||
"items": [
|
||||
{
|
||||
"item_code": item,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
|
||||
dn = create_delivery_note(
|
||||
item_code=item,
|
||||
company=company,
|
||||
customer=customer,
|
||||
qty=5,
|
||||
rate=500,
|
||||
warehouse=warehouse,
|
||||
target_warehouse=target,
|
||||
ignore_pricing_rule=0,
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
|
||||
dn.save()
|
||||
self.assertEqual(dn.ignore_pricing_rule, 1)
|
||||
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
|
||||
# rate should reset again if discounts are fiddled with
|
||||
dn.items[0].margin_type = "Amount"
|
||||
dn.items[0].margin_rate_or_amount = 50
|
||||
dn.save()
|
||||
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -586,8 +586,7 @@ $.extend(erpnext.item, {
|
||||
["parent","=", d.attribute]
|
||||
],
|
||||
fields: ["attribute_value"],
|
||||
limit_start: 0,
|
||||
limit_page_length: 500,
|
||||
limit_page_length: 0,
|
||||
parent: "Item Attribute",
|
||||
order_by: "idx"
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"
|
||||
|
||||
class MaterialRequest(BuyingController):
|
||||
def get_feed(self):
|
||||
return _("{0}: {1}").format(self.status, self.material_request_type)
|
||||
return
|
||||
|
||||
def check_if_already_pulled(self):
|
||||
pass
|
||||
|
@ -1285,6 +1285,14 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
from erpnext.accounts.party import add_party_account
|
||||
|
||||
add_party_account(
|
||||
"Supplier",
|
||||
"_Test Supplier USD",
|
||||
"_Test Company with perpetual inventory",
|
||||
"_Test Payable USD - TCP1",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
@ -1293,6 +1301,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
expense_account="_Test Account Cost for Goods Sold - TCP1",
|
||||
currency="USD",
|
||||
conversion_rate=70,
|
||||
supplier="_Test Supplier USD",
|
||||
)
|
||||
|
||||
pr = create_purchase_receipt(pi.name)
|
||||
|
@ -3,9 +3,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
||||
from frappe.utils.user import get_users_with_role
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
|
||||
@ -15,6 +17,8 @@ from erpnext.stock.stock_ledger import (
|
||||
repost_future_sle,
|
||||
)
|
||||
|
||||
RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError)
|
||||
|
||||
|
||||
class RepostItemValuation(Document):
|
||||
def validate(self):
|
||||
@ -132,7 +136,7 @@ def repost(doc):
|
||||
|
||||
doc.set_status("Completed")
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
traceback = frappe.get_traceback()
|
||||
doc.log_error("Unable to repost item valuation")
|
||||
@ -142,9 +146,9 @@ def repost(doc):
|
||||
message += "<br>" + "Traceback: <br>" + traceback
|
||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
raise
|
||||
if not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
@ -298,19 +298,17 @@ class StockEntry(StockController):
|
||||
for_update=True,
|
||||
)
|
||||
|
||||
for f in (
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"description",
|
||||
"item_name",
|
||||
"expense_account",
|
||||
"cost_center",
|
||||
"conversion_factor",
|
||||
):
|
||||
if f == "stock_uom" or not item.get(f):
|
||||
item.set(f, item_details.get(f))
|
||||
if f == "conversion_factor" and item.uom == item_details.get("stock_uom"):
|
||||
item.set(f, item_details.get(f))
|
||||
reset_fields = ("stock_uom", "item_name")
|
||||
for field in reset_fields:
|
||||
item.set(field, item_details.get(field))
|
||||
|
||||
update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
|
||||
|
||||
for field in update_fields:
|
||||
if not item.get(field):
|
||||
item.set(field, item_details.get(field))
|
||||
if field == "conversion_factor" and item.uom == item_details.get("stock_uom"):
|
||||
item.set(field, item_details.get(field))
|
||||
|
||||
if not item.transfer_qty and item.qty:
|
||||
item.transfer_qty = flt(
|
||||
@ -672,7 +670,8 @@ class StockEntry(StockController):
|
||||
batch_no=d.batch_no,
|
||||
)
|
||||
|
||||
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
|
||||
# do not round off basic rate to avoid precision loss
|
||||
d.basic_rate = flt(d.basic_rate)
|
||||
if d.is_process_loss:
|
||||
d.basic_rate = flt(0.0)
|
||||
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
||||
@ -720,7 +719,7 @@ class StockEntry(StockController):
|
||||
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
|
||||
return flt(outgoing_items_cost / total_fg_qty)
|
||||
|
||||
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
|
||||
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
|
||||
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
|
||||
|
||||
# Get raw materials cost from BOM if multiple material consumption entries
|
||||
@ -760,10 +759,8 @@ class StockEntry(StockController):
|
||||
for d in self.get("items"):
|
||||
if d.transfer_qty:
|
||||
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
|
||||
d.valuation_rate = flt(
|
||||
flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)),
|
||||
d.precision("valuation_rate"),
|
||||
)
|
||||
# Do not round off valuation rate to avoid precision loss
|
||||
d.valuation_rate = flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty))
|
||||
|
||||
def set_total_incoming_outgoing_value(self):
|
||||
self.total_incoming_value = self.total_outgoing_value = 0.0
|
||||
@ -1142,7 +1139,7 @@ class StockEntry(StockController):
|
||||
if self.job_card:
|
||||
job_doc = frappe.get_doc("Job Card", self.job_card)
|
||||
job_doc.set_transferred_qty(update_status=True)
|
||||
job_doc.set_transferred_qty_in_job_card(self)
|
||||
job_doc.set_transferred_qty_in_job_card_item(self)
|
||||
|
||||
if self.work_order:
|
||||
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
|
@ -2,8 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
@ -12,6 +10,7 @@ from frappe.utils import flt, nowdate, nowtime
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
create_item,
|
||||
make_item,
|
||||
make_item_variant,
|
||||
set_item_variant_settings,
|
||||
)
|
||||
@ -1443,6 +1442,21 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(mapped_se.items[0].basic_rate, 100)
|
||||
self.assertEqual(mapped_se.items[0].basic_amount, 200)
|
||||
|
||||
def test_stock_entry_item_details(self):
|
||||
item = make_item()
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item.name, qty=1, to_warehouse="_Test Warehouse - _TC", do_not_submit=True
|
||||
)
|
||||
|
||||
self.assertEqual(se.items[0].item_name, item.item_name)
|
||||
se.items[0].item_name = "wat"
|
||||
se.items[0].stock_uom = "Kg"
|
||||
se.save()
|
||||
|
||||
self.assertEqual(se.items[0].item_name, item.item_name)
|
||||
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -8,9 +8,8 @@ import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.utils.data import add_to_date
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, flt, today
|
||||
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
@ -1219,6 +1218,41 @@ class TestStockLedgerEntry(FrappeTestCase):
|
||||
except Exception as e:
|
||||
self.fail("Double processing of qty for clashing timestamp.")
|
||||
|
||||
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
|
||||
def test_transfer_invariants(self):
|
||||
"""Extact stock value should be transferred."""
|
||||
|
||||
item = make_item(
|
||||
properties={
|
||||
"valuation_method": "Moving Average",
|
||||
"stock_uom": "Kg",
|
||||
}
|
||||
).name
|
||||
source_warehouse = "Stores - TCP1"
|
||||
target_warehouse = "Finished Goods - TCP1"
|
||||
|
||||
make_purchase_receipt(
|
||||
item=item,
|
||||
warehouse=source_warehouse,
|
||||
qty=20,
|
||||
conversion_factor=1000,
|
||||
uom="Tonne",
|
||||
rate=156_526.0,
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
transfer = make_stock_entry(
|
||||
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
|
||||
)
|
||||
|
||||
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0}
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["*"],
|
||||
filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation",
|
||||
)
|
||||
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
|
||||
|
||||
|
||||
def create_repack_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -199,7 +199,7 @@ def process_args(args):
|
||||
if not args.get("price_list"):
|
||||
args.price_list = args.get("selling_price_list") or args.get("buying_price_list")
|
||||
|
||||
if args.barcode:
|
||||
if not args.item_code and args.barcode:
|
||||
args.item_code = get_item_code(barcode=args.barcode)
|
||||
elif not args.item_code and args.serial_no:
|
||||
args.item_code = get_item_code(serial_no=args.serial_no)
|
||||
|
@ -252,11 +252,14 @@ def notify_errors(exceptions_list):
|
||||
)
|
||||
|
||||
for exception in exceptions_list:
|
||||
exception = json.loads(exception)
|
||||
error_message = """<div class='small text-muted'>{0}</div><br>""".format(
|
||||
_(exception.get("message"))
|
||||
)
|
||||
content += error_message
|
||||
try:
|
||||
exception = json.loads(exception)
|
||||
error_message = """<div class='small text-muted'>{0}</div><br>""".format(
|
||||
_(exception.get("message"))
|
||||
)
|
||||
content += error_message
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
content += _("Regards,") + "<br>" + _("Administrator")
|
||||
|
||||
|
@ -1,24 +1,29 @@
|
||||
<h4>{{_("Request for Quotation")}}</h4>
|
||||
<p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p>
|
||||
<p>{{ message }}</p>
|
||||
|
||||
<p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p>
|
||||
<p>
|
||||
<button style="border: 1px solid #15c; padding: 6px; border-radius: 5px; background-color: white;">
|
||||
<a href="{{ rfq_link }}" style="color: #15c; text-decoration:none;" target="_blank">Submit your Quotation</a>
|
||||
</button>
|
||||
</p><br>
|
||||
|
||||
<p>{{_("Regards")}},<br>
|
||||
{{ user_fullname }}</p><br>
|
||||
|
||||
<br>
|
||||
<a
|
||||
href="{{ rfq_link }}"
|
||||
class="btn btn-default btn-sm"
|
||||
target="_blank">
|
||||
{{ _("Submit your Quotation") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% if update_password_link %}
|
||||
|
||||
<br>
|
||||
<p>{{_("Please click on the following button to set your new password")}}:</p>
|
||||
<p>
|
||||
<button style="border: 1px solid #15c; padding: 4px; border-radius: 5px; background-color: white;">
|
||||
<a href="{{ update_password_link }}" style="color: #15c; font-size: 12px; text-decoration:none;" target="_blank">{{_("Update Password") }}</a>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="{{ update_password_link }}"
|
||||
class="btn btn-default btn-xs"
|
||||
target="_blank">
|
||||
{{_("Set Password") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% endif %}
|
||||
<p>
|
||||
{{_("Regards")}},<br>
|
||||
{{ user_fullname }}
|
||||
</p>
|
||||
|
@ -40,3 +40,8 @@ class TestInit(unittest.TestCase):
|
||||
enc_name == expected_names[i],
|
||||
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]),
|
||||
)
|
||||
|
||||
def test_translation_files(self):
|
||||
from frappe.tests.test_translate import verify_translation_files
|
||||
|
||||
verify_translation_files("erpnext")
|
||||
|
@ -8,6 +8,7 @@ class TestSearch(unittest.TestCase):
|
||||
# Search for the word "cond", part of the word "conduire" (Lead) in french.
|
||||
def test_contact_search_in_foreign_language(self):
|
||||
try:
|
||||
frappe.local.lang_full_dict = None # reset cached translations
|
||||
frappe.local.lang = "fr"
|
||||
output = filter_dynamic_link_doctypes(
|
||||
"DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"}
|
||||
|
@ -1701,7 +1701,7 @@ No Permission,Keine Berechtigung,
|
||||
No Remarks,Keine Anmerkungen,
|
||||
No Result to submit,Kein Ergebnis zur Einreichung,
|
||||
No Salary Structure assigned for Employee {0} on given date {1},Keine Gehaltsstruktur für Mitarbeiter {0} am angegebenen Datum {1} zugewiesen,
|
||||
No Staffing Plans found for this Designation,Für diese Bezeichnung wurden keine Stellenpläne gefunden,
|
||||
No Staffing Plans found for this Designation,Für diese Position wurden keine Stellenpläne gefunden,
|
||||
No Student Groups created.,Keine Studentengruppen erstellt.,
|
||||
No Students in,Keine Studenten in,
|
||||
No Tax Withholding data found for the current Fiscal Year.,Keine Steuerverweigerungsdaten für das aktuelle Geschäftsjahr gefunden.,
|
||||
@ -2027,7 +2027,7 @@ Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine
|
||||
Please select Category first,Bitte zuerst Kategorie auswählen,
|
||||
Please select Charge Type first,Bitte zuerst Chargentyp auswählen,
|
||||
Please select Company,Bitte Unternehmen auswählen,
|
||||
Please select Company and Designation,Bitte wählen Sie Unternehmen und Stelle,
|
||||
Please select Company and Designation,Bitte wählen Sie Unternehmen und Position,
|
||||
Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten",
|
||||
Please select Company first,Bitte zuerst Unternehmen auswählen,
|
||||
Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert,
|
||||
@ -2772,7 +2772,7 @@ Split,Teilt,
|
||||
Split Batch,Split Batch,
|
||||
Split Issue,Split-Problem,
|
||||
Sports,Sport,
|
||||
Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Bezeichnung {1},
|
||||
Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Position {1},
|
||||
Standard,Standard,
|
||||
Standard Buying,Standard-Kauf,
|
||||
Standard Selling,Standard-Vertrieb,
|
||||
@ -3710,7 +3710,7 @@ Delivered Quantity,Gelieferte Menge,
|
||||
Delivery Notes,Lieferscheine,
|
||||
Depreciated Amount,Abschreibungsbetrag,
|
||||
Description,Beschreibung,
|
||||
Designation,Bezeichnung,
|
||||
Designation,Position,
|
||||
Difference Value,Differenzwert,
|
||||
Dimension Filter,Dimensionsfilter,
|
||||
Disabled,Deaktiviert,
|
||||
@ -3920,7 +3920,7 @@ Please enter <b>Difference Account</b> or set default <b>Stock Adjustment Accoun
|
||||
Please enter GSTIN and state for the Company Address {0},Bitte geben Sie GSTIN ein und geben Sie die Firmenadresse {0} an.,
|
||||
Please enter Item Code to get item taxes,"Bitte geben Sie den Artikelcode ein, um die Artikelsteuern zu erhalten",
|
||||
Please enter Warehouse and Date,Bitte geben Sie Lager und Datum ein,
|
||||
Please enter the designation,Bitte geben Sie die Bezeichnung ein,
|
||||
Please enter the designation,Bitte geben Sie die Position ein,
|
||||
Please login as a Marketplace User to edit this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu bearbeiten.",
|
||||
Please login as a Marketplace User to report this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu melden.",
|
||||
Please select <b>Template Type</b> to download template,"Bitte wählen Sie <b>Vorlagentyp</b> , um die Vorlage herunterzuladen",
|
||||
@ -5063,7 +5063,7 @@ Accepted Qty,Akzeptierte Menge,
|
||||
Rejected Qty,Abgelehnt Menge,
|
||||
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
|
||||
Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
|
||||
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
|
||||
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
|
||||
Rate ,Preis,
|
||||
Rate (Company Currency),Preis (Unternehmenswährung),
|
||||
Amount (Company Currency),Betrag (Unternehmenswährung),
|
||||
@ -6243,7 +6243,7 @@ Checking this will create Lab Test(s) specified in the Sales Invoice on submissi
|
||||
Create Sample Collection document for Lab Test,Erstellen Sie ein Probensammeldokument für den Labortest,
|
||||
Checking this will create a Sample Collection document every time you create a Lab Test,"Wenn Sie dies aktivieren, wird jedes Mal, wenn Sie einen Labortest erstellen, ein Probensammeldokument erstellt",
|
||||
Employee name and designation in print,Name und Bezeichnung des Mitarbeiters im Druck,
|
||||
Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Bezeichnung des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.",
|
||||
Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Position des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.",
|
||||
Do not print or email Lab Tests without Approval,Drucken oder senden Sie Labortests nicht ohne Genehmigung per E-Mail,
|
||||
Checking this will restrict printing and emailing of Lab Test documents unless they have the status as Approved.,"Wenn Sie dies aktivieren, wird das Drucken und E-Mailen von Labortestdokumenten eingeschränkt, sofern diese nicht den Status "Genehmigt" haben.",
|
||||
Custom Signature in Print,Kundenspezifische Unterschrift im Druck,
|
||||
@ -6499,7 +6499,7 @@ Department Approver,Abteilungsgenehmiger,
|
||||
Approver,Genehmiger,
|
||||
Required Skills,Benötigte Fähigkeiten,
|
||||
Skills,Kompetenzen,
|
||||
Designation Skill,Bezeichnung Fähigkeit,
|
||||
Designation Skill,Positions Fähigkeit,
|
||||
Skill,Fertigkeit,
|
||||
Driver,Fahrer/-in,
|
||||
HR-DRI-.YYYY.-,HR-DRI-.YYYY.-,
|
||||
@ -6517,20 +6517,20 @@ Driver licence class,Führerscheinklasse,
|
||||
HR-EMP-,HR-EMP-,
|
||||
Employment Type,Art der Beschäftigung,
|
||||
Emergency Contact,Notfallkontakt,
|
||||
Emergency Contact Name,Notfall Kontaktname,
|
||||
Emergency Phone,Notruf,
|
||||
Emergency Contact Name,Name des Notfallkontakts,
|
||||
Emergency Phone,Telefonnummer des Notfallkontakts,
|
||||
ERPNext User,ERPNext Benutzer,
|
||||
"System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.",
|
||||
Create User Permission,Benutzerberechtigung Erstellen,
|
||||
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
|
||||
Joining Details,Details des Beitritts,
|
||||
Offer Date,Angebotsdatum,
|
||||
Confirmation Date,Datum bestätigen,
|
||||
Confirmation Date,Bestätigungsdatum,
|
||||
Contract End Date,Vertragsende,
|
||||
Notice (days),Meldung(s)(-Tage),
|
||||
Notice (days),Kündigungsfrist (Tage),
|
||||
Date Of Retirement,Zeitpunkt der Pensionierung,
|
||||
Department and Grade,Abteilung und Klasse,
|
||||
Reports to,Berichte an,
|
||||
Reports to,Vorgesetzter,
|
||||
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
||||
Leave Policy,Urlaubsrichtlinie,
|
||||
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
||||
@ -6553,8 +6553,8 @@ Company Email,E-Mail-Adresse des Unternehmens,
|
||||
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
|
||||
Current Address Is,Aktuelle Adresse ist,
|
||||
Current Address,Aktuelle Adresse,
|
||||
Personal Bio,Persönliches Bio,
|
||||
Bio / Cover Letter,Bio / Anschreiben,
|
||||
Personal Bio,Lebenslauf,
|
||||
Bio / Cover Letter,Lebenslauf / Anschreiben,
|
||||
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
|
||||
Passport Number,Passnummer,
|
||||
Date of Issue,Ausstellungsdatum,
|
||||
@ -6798,7 +6798,7 @@ Select Employees,Mitarbeiter auswählen,
|
||||
Employment Type (optional),Anstellungsart (optional),
|
||||
Branch (optional),Zweigstelle (optional),
|
||||
Department (optional),Abteilung (optional),
|
||||
Designation (optional),Bezeichnung (optional),
|
||||
Designation (optional),Position (optional),
|
||||
Employee Grade (optional),Dienstgrad (optional),
|
||||
Employee (optional),Mitarbeiter (optional),
|
||||
Allocate Leaves,Blätter zuweisen,
|
||||
@ -7769,7 +7769,7 @@ Authorized Value,Autorisierter Wert,
|
||||
Applicable To (Role),Anwenden auf (Rolle),
|
||||
Applicable To (Employee),Anwenden auf (Mitarbeiter),
|
||||
Applicable To (User),Anwenden auf (Benutzer),
|
||||
Applicable To (Designation),Anwenden auf (Bezeichnung),
|
||||
Applicable To (Designation),Anwenden auf (Position),
|
||||
Approving Role (above authorized value),Genehmigende Rolle (über dem autorisierten Wert),
|
||||
Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert),
|
||||
Brand Defaults,Markenstandards,
|
||||
@ -8946,7 +8946,7 @@ Requesting Practitioner,Praktizierender anfordern,
|
||||
Requesting Department,Abteilung anfordern,
|
||||
Employee (Lab Technician),Mitarbeiter (Labortechniker),
|
||||
Lab Technician Name,Name des Labortechnikers,
|
||||
Lab Technician Designation,Bezeichnung des Labortechnikers,
|
||||
Lab Technician Designation,Position des Labortechnikers,
|
||||
Compound Test Result,Zusammengesetztes Testergebnis,
|
||||
Organism Test Result,Organismustestergebnis,
|
||||
Sensitivity Test Result,Empfindlichkeitstestergebnis,
|
||||
@ -9542,7 +9542,7 @@ Preview Email,Vorschau E-Mail,
|
||||
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
|
||||
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
|
||||
"Home, Work, etc.","Zuhause, Arbeit usw.",
|
||||
Exit Interview Held On,Beenden Sie das Interview,
|
||||
Exit Interview Held On,Entlassungsgespräch am,
|
||||
Condition and formula,Zustand und Formel,
|
||||
Sets 'Target Warehouse' in each row of the Items table.,Legt '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.,
|
||||
|
Can't render this file because it is too large.
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user