Merge branch 'develop' into loan_doc_reco_queries

This commit is contained in:
Deepesh Garg 2022-05-25 18:50:54 +05:30 committed by GitHub
commit d68187caae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 146604 additions and 663 deletions

View File

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

View File

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

View File

@ -94,7 +94,7 @@ class JournalEntry(AccountsController):
unlink_ref_doc_from_payment_entries(self) unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name) 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.make_gl_entries(1)
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()

View File

@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
self.set_status() self.set_status()
def on_cancel(self): 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.make_gl_entries(cancel=1)
self.update_expense_claim() self.update_expense_claim()
self.update_outstanding_amounts() 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: if ref_doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name)) frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))

View File

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

View File

@ -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) {
// }
});

View File

@ -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": []
}

View File

@ -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()

View File

@ -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])

View File

@ -78,6 +78,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
rate=400, rate=400,
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
) )
create_sales_invoice( create_sales_invoice(
company=company, company=company,
@ -86,6 +88,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
rate=200, rate=200,
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
) )
pcv = self.make_period_closing_voucher(submit=False) pcv = self.make_period_closing_voucher(submit=False)
@ -119,14 +123,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice( si = create_sales_invoice(
company=company, company=company,
income_account="Sales - TPC", income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center, cost_center=cost_center,
rate=400, rate=400,
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
) )
jv = make_journal_entry( jv = make_journal_entry(
account1="Cash - TPC", account1="Cash - TPC",
account2="Sales - TPC", account2="Sales - TPC",

View File

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

View File

@ -96,6 +96,7 @@ class POSInvoice(SalesInvoice):
) )
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = "Payment Ledger Entry"
# run on cancel method of selling controller # run on cancel method of selling controller
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
if not self.is_return and self.loyalty_program: if not self.is_return and self.loyalty_program:

View File

@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase):
title="_Test Pricing Rule with Min Qty - 2", 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 = si.items[0]
item.stock_qty = 1 item.stock_qty = 1
si.save() si.save()

View File

@ -1418,7 +1418,12 @@ class PurchaseInvoice(BuyingController):
frappe.db.set(self, "status", "Cancelled") frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) 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) self.update_advance_tax_references(cancel=1)
def update_project(self): def update_project(self):

View File

@ -396,7 +396,12 @@ class SalesInvoice(SellingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.unlink_sales_invoice_from_timesheets() 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): def update_status_updater_args(self):
if cint(self.update_stock): if cint(self.update_stock):

View File

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

View File

@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError): class ClosedAccountingPeriod(frappe.ValidationError):
@ -31,8 +32,10 @@ def make_gl_entries(
if gl_map: if gl_map:
if not cancel: if not cancel:
validate_accounting_period(gl_map) validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: if gl_map and len(gl_map) > 1:
create_payment_ledger_entry(gl_map)
save_entries(gl_map, adv_adj, update_outstanding, from_repost) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries # Post GL Map proccess there may no be any GL Entries
elif gl_map: 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) make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
Account = frappe.qb.DocType("Account")
disabled_accounts = (
frappe.qb.from_(Account)
.where(Account.name.isin(accounts) & Account.disabled == 1)
.select(Account.name, Account.disabled)
).run(as_dict=True)
if disabled_accounts:
account_list = "<br>"
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
frappe.throw(
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
title=_("Disabled Account Selected"),
)
def validate_accounting_period(gl_map): def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql( accounting_periods = frappe.db.sql(
""" SELECT """ SELECT
@ -458,6 +481,7 @@ def make_reverse_gl_entries(
).run(as_dict=1) ).run(as_dict=1)
if gl_entries: if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
validate_accounting_period(gl_entries) validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj) check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])

View File

@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
return None return None
else: else:
return None 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()

View File

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

View File

@ -275,7 +275,7 @@ def get_conditions(filters):
) )
conditions.append("{0} in %({0})s".format(dimension.fieldname)) conditions.append("{0} in %({0})s".format(dimension.fieldname))
else: else:
conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) conditions.append("{0} in %({0})s".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from typing import List, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe import _, throw from frappe import _, qb, throw
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate 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 # imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency # noqa 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 import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on from erpnext.stock.utils import get_stock_value_on
@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report):
if icons: if icons:
for icon in icons: for icon in icons:
frappe.delete_doc("Desktop Icon", icon) 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()

View File

@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = "Payment Ledger Entry"
super(PurchaseOrder, self).on_cancel() super(PurchaseOrder, self).on_cancel()
if self.is_against_so(): if self.is_against_so():

View File

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

View File

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

View File

@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
from erpnext.accounts.party import ( from erpnext.accounts.party import (
get_party_account, get_party_account,
get_party_account_currency, get_party_account_currency,
get_party_gle_currency,
validate_party_frozen_disabled, validate_party_frozen_disabled,
) )
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year 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.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
self.set_incoming_rate() self.set_incoming_rate()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
@ -167,6 +169,7 @@ class AccountsController(TransactionBase):
self.validate_party() self.validate_party()
self.validate_currency() self.validate_currency()
self.validate_party_account_currency()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]: if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" 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") msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing")) 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): def validate_due_date(self):
if self.get("is_pos"): if self.get("is_pos"):
return return
@ -1121,11 +1132,10 @@ class AccountsController(TransactionBase):
{ {
"account": item.discount_account, "account": item.discount_account,
"against": supplier_or_customer, "against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision("discount_amount")), dr_or_cr: flt(
dr_or_cr
+ "_in_account_currency": flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount") 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, "cost_center": item.cost_center,
"project": item.project, "project": item.project,
}, },
@ -1140,11 +1150,11 @@ class AccountsController(TransactionBase):
{ {
"account": income_or_expense_account, "account": income_or_expense_account,
"against": supplier_or_customer, "against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")), rev_dr_cr: flt(
rev_dr_cr
+ "_in_account_currency": flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount") 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, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
}, },
@ -1439,6 +1449,27 @@ class AccountsController(TransactionBase):
# at quotation / sales order level and we shouldn't stop someone # at quotation / sales order level and we shouldn't stop someone
# from creating a sales invoice if sales order is already created # 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): def delink_advance_entries(self, linked_doc_name):
total_allocated_amount = 0 total_allocated_amount = 0
for adv in self.advances: for adv in self.advances:
@ -1738,6 +1769,8 @@ class AccountsController(TransactionBase):
internal_party_field = "is_internal_customer" internal_party_field = "is_internal_customer"
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
internal_party_field = "is_internal_supplier" internal_party_field = "is_internal_supplier"
else:
return False
if self.get(internal_party_field) and (self.represents_company == self.company): if self.get(internal_party_field) and (self.represents_company == self.company):
return True return True

View File

@ -307,14 +307,15 @@ class BuyingController(StockController, Subcontracting):
if self.is_internal_transfer(): if self.is_internal_transfer():
if rate != d.rate: if rate != d.rate:
d.rate = rate d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint( frappe.msgprint(
_( _(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx), ).format(d.idx),
alert=1, 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): def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0 supplied_items_cost = 0.0

View File

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

View File

@ -447,15 +447,16 @@ class SellingController(StockController):
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate")) rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
if d.rate != rate: if d.rate != rate:
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_percentage = 0.0
d.discount_amount = 0 d.discount_amount = 0.0
frappe.msgprint( d.margin_rate_or_amount = 0.0
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
elif self.get("return_against"): elif self.get("return_against"):
# Get incoming rate of return entry from reference document # Get incoming rate of return entry from reference document

View File

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

View File

@ -129,6 +129,7 @@ class TestShoppingCart(unittest.TestCase):
self.assertEqual(quotation.net_total, 20) self.assertEqual(quotation.net_total, 20)
self.assertEqual(len(quotation.get("items")), 1) self.assertEqual(len(quotation.get("items")), 1)
@unittest.skip("Flaky in CI")
def test_tax_rule(self): def test_tax_rule(self):
self.create_tax_rule() self.create_tax_rule()
self.login_as_customer() self.login_as_customer()

View File

@ -321,6 +321,7 @@ doc_events = {
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values", "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"]}, "POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
@ -486,6 +487,7 @@ communication_doctypes = ["Customer", "Supplier"]
accounting_dimension_doctypes = [ accounting_dimension_doctypes = [
"GL Entry", "GL Entry",
"Payment Ledger Entry",
"Sales Invoice", "Sales Invoice",
"Purchase Invoice", "Purchase Invoice",
"Payment Entry", "Payment Entry",

View File

@ -32,6 +32,9 @@ class Attendance(Document):
self.validate_employee_status() self.validate_employee_status()
self.check_leave_record() self.check_leave_record()
def on_cancel(self):
self.unlink_attendance_from_checkins()
def validate_attendance_date(self): def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
@ -127,6 +130,33 @@ class Attendance(Document):
if not emp: if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) 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): def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
attendance = frappe.qb.DocType("Attendance") attendance = frappe.qb.DocType("Attendance")

View File

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

View File

@ -227,11 +227,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
in_log = out_log = None in_log = out_log = None
if not in_log: if not in_log:
in_log = log if log.log_type == "IN" else None 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: elif not out_log:
out_log = log if log.log_type == "OUT" else None out_log = log if log.log_type == "OUT" else None
if in_log and out_log: if in_log and out_log:
out_time = out_log.time out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time) total_hours += time_diff_in_hours(in_log.time, out_log.time)
return total_hours, in_time, out_time return total_hours, in_time, out_time

View File

@ -76,6 +76,17 @@ class TestEmployeeCheckin(FrappeTestCase):
) )
self.assertEqual(attendance_count, 1) 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): def test_calculate_working_hours(self):
check_in_out_type = [ check_in_out_type = [
"Alternating entries as IN and OUT during the same shift", "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)) 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): def test_fetch_shift(self):
employee = make_employee("test_employee_checkin@example.com", company="_Test Company") employee = make_employee("test_employee_checkin@example.com", company="_Test Company")

View File

@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
def on_cancel(self): def on_cancel(self):
self.update_task_and_project() 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: if self.payable_account:
self.make_gl_entries(cancel=True) self.make_gl_entries(cancel=True)

View File

@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, nowdate 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.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.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import ( from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
@ -107,7 +107,10 @@ class LeaveEncashment(Document):
self.leave_balance = ( self.leave_balance = (
allocation.total_leaves_allocated allocation.total_leaves_allocated
- allocation.carry_forwarded_leaves_count - 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( encashable_days = self.leave_balance - frappe.db.get_value(
@ -126,14 +129,25 @@ class LeaveEncashment(Document):
return True return True
def get_leave_allocation(self): def get_leave_allocation(self):
leave_allocation = frappe.db.sql( date = self.encashment_date or getdate()
"""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}' LeaveAllocation = frappe.qb.DocType("Leave Allocation")
and employee= '{2}'""".format( leave_allocation = (
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee frappe.qb.from_(LeaveAllocation)
), .select(
as_dict=1, LeaveAllocation.name,
) # nosec 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 return leave_allocation[0] if leave_allocation else None

View File

@ -4,26 +4,42 @@
import unittest import unittest
import frappe 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.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_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.test_leave_policy import create_leave_policy
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees, 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 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): def setUp(self):
frappe.db.sql("""delete from `tabLeave Period`""") frappe.db.delete("Leave Period")
frappe.db.sql("""delete from `tabLeave Policy Assignment`""") frappe.db.delete("Leave Policy Assignment")
frappe.db.sql("""delete from `tabLeave Allocation`""") frappe.db.delete("Leave Allocation")
frappe.db.sql("""delete from `tabLeave Ledger Entry`""") frappe.db.delete("Leave Ledger Entry")
frappe.db.sql("""delete from `tabAdditional Salary`""") 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 # create the leave policy
leave_policy = create_leave_policy( leave_policy = create_leave_policy(
@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
leave_policy.submit() leave_policy.submit()
# create employee, salary structure and assignment # 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 = { data = {
"assignment_based_on": "Leave Period", "assignment_based_on": "Leave Period",
@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
other_details={"leave_encashment_amount_per_day": 50}, other_details={"leave_encashment_amount_per_day": 50},
) )
def tearDown(self): @set_holiday_list("_Test Leave Encashment", "_Test Company")
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)
def test_leave_balance_value_and_amount(self): def test_leave_balance_value_and_amount(self):
frappe.db.sql("""delete from `tabLeave Encashment`""")
leave_encashment = frappe.get_doc( leave_encashment = frappe.get_doc(
dict( dict(
doctype="Leave Encashment", doctype="Leave Encashment",
employee=self.employee, employee=self.employee,
leave_type="_Test Leave Type Encashment", leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name, leave_period=self.leave_period.name,
payroll_date=today(), encashment_date=self.leave_period.to_date,
currency="INR", currency="INR",
) )
).insert() ).insert()
@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
self.assertTrue(add_sal) self.assertTrue(add_sal)
def test_creation_of_leave_ledger_entry_on_submit(self): @set_holiday_list("_Test Leave Encashment", "_Test Company")
frappe.db.sql("""delete from `tabLeave Encashment`""") 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( leave_encashment = frappe.get_doc(
dict( dict(
doctype="Leave Encashment", doctype="Leave Encashment",
employee=self.employee, employee=self.employee,
leave_type="_Test Leave Type Encashment", leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name, 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", currency="INR",
) )
).insert() ).insert()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -264,6 +264,7 @@ class LoanRepayment(AccountsController):
regenerate_repayment_schedule(self.against_loan, cancel) regenerate_repayment_schedule(self.against_loan, cancel)
def allocate_amounts(self, repayment_details): def allocate_amounts(self, repayment_details):
precision = cint(frappe.db.get_default("currency_precision")) or 2
self.set("repayment_details", []) self.set("repayment_details", [])
self.principal_amount_paid = 0 self.principal_amount_paid = 0
self.total_penalty_paid = 0 self.total_penalty_paid = 0
@ -278,9 +279,9 @@ class LoanRepayment(AccountsController):
if interest_paid > 0: if interest_paid > 0:
if self.penalty_amount and interest_paid > self.penalty_amount: 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: elif self.penalty_amount:
self.total_penalty_paid = interest_paid self.total_penalty_paid = flt(interest_paid, precision)
interest_paid -= self.total_penalty_paid interest_paid -= self.total_penalty_paid
@ -447,8 +448,6 @@ class LoanRepayment(AccountsController):
"remarks": remarks, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date), "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 "",
} }
) )
) )

View File

@ -499,15 +499,11 @@ cur_frm.cscript.qty = function(doc) {
cur_frm.cscript.rate = function(doc, cdt, cdn) { cur_frm.cscript.rate = function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
var scrap_items = false; const is_scrap_item = cdt == "BOM Scrap Item";
if(cdt == 'BOM Scrap Item') {
scrap_items = true;
}
if (d.bom_no) { if (d.bom_no) {
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item.")); 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 { } else {
erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_rm_cost(doc);
erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc);

View File

@ -33,7 +33,6 @@
"amount", "amount",
"base_amount", "base_amount",
"section_break_18", "section_break_18",
"scrap",
"qty_consumed_per_unit", "qty_consumed_per_unit",
"section_break_27", "section_break_27",
"has_variants", "has_variants",
@ -223,15 +222,6 @@
"fieldname": "section_break_18", "fieldname": "section_break_18",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{
"columns": 1,
"fieldname": "scrap",
"fieldtype": "Float",
"label": "Scrap %",
"oldfieldname": "scrap",
"oldfieldtype": "Currency",
"print_hide": 1
},
{ {
"fieldname": "qty_consumed_per_unit", "fieldname": "qty_consumed_per_unit",
"fieldtype": "Float", "fieldtype": "Float",
@ -298,7 +288,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-24 16:57:57.020232", "modified": "2022-05-19 02:32:43.785470",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Item", "name": "BOM Item",

View File

@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
pass pass
class JobCardOverTransferError(frappe.ValidationError):
pass
class JobCard(Document): class JobCard(Document):
def onload(self): def onload(self):
excess_transfer = frappe.db.get_single_value( 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: for row in ste_doc.items:
if not row.job_card_item: if not row.job_card_item:
continue continue
qty = frappe.db.sql( sed = frappe.qb.DocType("Stock Entry Detail")
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se se = frappe.qb.DocType("Stock Entry")
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and transferred_qty = (
se.purpose = 'Material Transfer for Manufacture' frappe.qb.from_(sed)
""", .join(se)
(row.job_card_item), .on(sed.parent == se.name)
)[0][0] .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): 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: if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 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("time_logs", [])
target.set("employee", []) target.set("employee", [])
target.set("items", []) target.set("items", [])
target.set("sub_operations", [])
target.set_sub_operations() target.set_sub_operations()
target.get_required_items() target.get_required_items()
target.validate_time_logs() target.validate_time_logs()

View File

@ -1,15 +1,25 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # 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 ( from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc, 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.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.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry 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): class TestJobCard(FrappeTestCase):
def setUp(self): def setUp(self):
make_bom_for_jc_tests() 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",) def generate_required_stock(self, work_order: WorkOrder) -> None:
tests_that_transfer_against_jc = ( """Create twice the stock for all required items in work order."""
"test_job_card_multiple_materials_transfer", for item in work_order.required_items:
"test_job_card_excess_material_transfer", make_stock_entry(
"test_job_card_partial_material_transfer", item_code=item.item_code,
) target=item.source_warehouse or self.source_warehouse,
qty=item.required_qty * 2,
if self._testMethodName in tests_that_skip_setup: basic_rate=100,
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 tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def test_job_card(self): def test_job_card_operations(self):
job_cards = frappe.get_all( job_cards = frappe.get_all(
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] "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" doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save) 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): def test_job_card_with_different_work_station(self):
job_cards = frappe.get_all( job_cards = frappe.get_all(
"Job Card", "Job Card",
@ -96,19 +105,11 @@ class TestJobCard(FrappeTestCase):
) )
self.assertEqual(completed_qty, job_card.for_quantity) 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): def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) 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}) jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name}) jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
employee = "_T-Employee-00001" # from test records employee = "_T-Employee-00001" # from test records
@ -137,10 +138,10 @@ class TestJobCard(FrappeTestCase):
def test_job_card_multiple_materials_transfer(self): def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs." "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) self.transfer_material_against = "Job Card"
make_stock_entry( self.source_warehouse = "Stores - _TC"
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, 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_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_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 # transfer was made for 2 fg qty in first transfer Stock Entry
self.assertEqual(transfer_entry_2.fg_completed_qty, 0) 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): def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card." "Test transferring more than required RM against Job Card."
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) self.transfer_material_against = "Job Card"
make_stock_entry( self.source_warehouse = "Stores - _TC"
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
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_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 # fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name) 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[0].qty = 5
transfer_entry_2.items[1].qty = 3 transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert() transfer_entry_2.insert()
transfer_entry_2.submit() self.assertRaises(JobCardOverTransferError, 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")
def test_job_card_partial_material_transfer(self): def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card" "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) self.generate_required_stock(self.work_order)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
# partially transfer # 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.fg_completed_qty = 1
transfer_entry.get_items() transfer_entry.get_items()
transfer_entry.insert() transfer_entry.insert()
@ -232,7 +255,7 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(transfer_entry.items[1].qty, 3) self.assertEqual(transfer_entry.items[1].qty, 3)
# transfer remaining # 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.fg_completed_qty, 1)
self.assertEqual(transfer_entry_2.items[0].qty, 5) 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].item_code, "_Test Item")
self.assertEqual(transfer_entry.items[0].qty, 2) 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(): def create_bom_with_multiple_operations():

View File

@ -21,7 +21,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all( exploded_items = frappe.get_all(
"BOM Item", "BOM Item",
filters={"parent": bom}, 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: for item in exploded_items:
@ -37,7 +37,6 @@ def get_exploded_items(bom, data, indent=0, qty=1):
"qty": item.qty * qty, "qty": item.qty * qty,
"uom": item.uom, "uom": item.uom,
"description": item.description, "description": item.description,
"scrap": item.scrap,
} }
) )
if item.bom_no: if item.bom_no:
@ -64,5 +63,4 @@ def get_columns():
"fieldname": "description", "fieldname": "description",
"width": 150, "width": 150,
}, },
{"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100},
] ]

View File

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

View 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

View 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)

View File

@ -16,6 +16,7 @@ from frappe.utils import (
comma_and, comma_and,
date_diff, date_diff,
flt, flt,
get_link_to_form,
getdate, getdate,
) )
@ -45,6 +46,7 @@ class PayrollEntry(Document):
def before_submit(self): def before_submit(self):
self.validate_employee_details() self.validate_employee_details()
self.validate_payroll_payable_account()
if self.validate_attendance: if self.validate_attendance:
if self.validate_employee_attendance(): if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance")) frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
@ -66,6 +68,14 @@ class PayrollEntry(Document):
if len(emp_with_sal_slip): if len(emp_with_sal_slip):
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(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): def on_cancel(self):
frappe.delete_doc( frappe.delete_doc(
"Salary Slip", "Salary Slip",

View File

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

View File

@ -90,7 +90,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
else { else {
return{ return{
query: "erpnext.controllers.queries.item_query", 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}
} }
} }
}); });

View File

@ -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)) { if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
$.each(this.frm.doc['payments'] || [], function(index, data) { $.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) { 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); 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); frappe.model.set_value(data.doctype, data.name, "amount", amount);
payment_status = false; payment_status = false;
} else if(me.frm.doc.paid_amount) { } else if(me.frm.doc.paid_amount) {
frappe.model.set_value(data.doctype, data.name, "amount", 0.0); frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
} }

View File

@ -923,12 +923,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
currency() { currency() {
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */ // The transaction date be either transaction_date (from orders) or posting_date (from invoices)
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date; let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
/* end manqala */
var me = this; let me = this;
this.set_dynamic_labels(); this.set_dynamic_labels();
var company_currency = this.get_company_currency(); let company_currency = this.get_company_currency();
// Added `ignore_price_list` to determine if document is loading after mapping from another doc // Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { && !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
@ -942,7 +942,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
}); });
} else { } else {
this.conversion_rate(); // company currency and doc currency is same
// this will prevent unnecessary conversion rate triggers
this.frm.set_value("conversion_rate", 1.0);
} }
} }

View File

@ -125,7 +125,7 @@ $.extend(erpnext.utils, {
}, },
add_indicator_for_multicompany: function(frm, info) { 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.addClass('flex');
frm.dashboard.stats_area_row.css('flex-wrap', 'wrap'); frm.dashboard.stats_area_row.css('flex-wrap', 'wrap');
@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
filters.splice(index, 0, { filters.splice(index, 0, {
"fieldname": dimension["fieldname"], "fieldname": dimension["fieldname"],
"label": __(dimension["label"]), "label": __(dimension["label"]),
"fieldtype": "Link", "fieldtype": "MultiSelectList",
"options": dimension["document_type"] get_data: function(txt) {
return frappe.db.get_link_options(dimension["document_type"], txt);
},
}); });
} }
}); });

View File

@ -31,30 +31,39 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
} }
process_scan() { process_scan() {
let me = this; return new Promise((resolve, reject) => {
let me = this;
const input = this.scan_barcode_field.value; const input = this.scan_barcode_field.value;
if (!input) { if (!input) {
return; return;
} }
frappe frappe
.call({ .call({
method: this.scan_api, method: this.scan_api,
args: { args: {
search_value: input, search_value: input,
}, },
}) })
.then((r) => { .then((r) => {
const data = r && r.message; const data = r && r.message;
if (!data || Object.keys(data).length === 0) { if (!data || Object.keys(data).length === 0) {
this.show_alert(__("Cannot find Item with this Barcode"), "red"); this.show_alert(__("Cannot find Item with this Barcode"), "red");
this.clean_up(); this.clean_up();
return; reject();
} return;
}
me.update_table(data); const row = me.update_table(data);
}); if (row) {
resolve(row);
}
else {
reject();
}
});
});
} }
update_table(data) { update_table(data) {
@ -90,6 +99,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.set_batch_no(row, batch_no); this.set_batch_no(row, batch_no);
this.set_barcode(row, barcode); this.set_barcode(row, barcode);
this.clean_up(); this.clean_up();
return row;
} }
// batch and serial selector is reduandant when all info can be added by scan // batch and serial selector is reduandant when all info can be added by scan

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "field:hsn_code", "autoname": "field:hsn_code",
"creation": "2017-06-21 10:48:56.422086", "creation": "2017-06-21 10:48:56.422086",
"doctype": "DocType", "doctype": "DocType",
@ -7,6 +8,7 @@
"field_order": [ "field_order": [
"hsn_code", "hsn_code",
"description", "description",
"gst_rates",
"taxes" "taxes"
], ],
"fields": [ "fields": [
@ -16,22 +18,37 @@
"in_list_view": 1, "in_list_view": 1,
"label": "HSN Code", "label": "HSN Code",
"reqd": 1, "reqd": 1,
"show_days": 1,
"show_seconds": 1,
"unique": 1 "unique": 1
}, },
{ {
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"in_list_view": 1, "in_list_view": 1,
"label": "Description" "label": "Description",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "taxes", "fieldname": "taxes",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Taxes", "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", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "GST HSN Code", "name": "GST HSN Code",

View File

@ -3,7 +3,7 @@
frappe.ui.form.on('GST Settings', { frappe.ui.form.on('GST Settings', {
refresh: function(frm) { refresh: function(frm) {
frm.add_custom_button('Send GST Update Reminder', () => { frm.add_custom_button(__('Send GST Update Reminder'), () => {
return new Promise((resolve) => { return new Promise((resolve) => {
return frappe.call({ return frappe.call({
method: 'erpnext.regional.doctype.gst_settings.gst_settings.send_reminder' 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( $(frm.fields_dict.gst_summary.wrapper).empty().html(
`<table class="table table-bordered"> `<table class="table table-bordered">
<tbody> <tbody>

View File

@ -2,13 +2,14 @@
# For license information, please see license.txt # For license information, please see license.txt
import json
import os import os
import frappe import frappe
from frappe import _ from frappe import _
from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.document import Document 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): 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 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

File diff suppressed because it is too large Load Diff

View 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"
}

View 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

View File

@ -149,58 +149,27 @@ erpnext.setup_einvoice_actions = (doctype) => {
} }
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => { const action = () => {
const d = new frappe.ui.Dialog({ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
title: __('Cancel E-Way Bill'), message += '<br><br>';
fields: fields, message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
primary_action: function() {
const data = d.get_values(); const dialog = frappe.msgprint({
frappe.call({ title: __('Update E-Way Bill Cancelled Status?'),
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', message: message,
args: { indicator: 'orange',
doctype, primary_action: {
docname: name, action: function() {
eway_bill: ewaybill, frappe.call({
reason: data.reason.split('-')[0], method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
remark: data.remark args: { doctype, docname: name },
}, freeze: true,
freeze: true, callback: () => frm.reload_doc() && dialog.hide()
callback: () => { });
frappe.show_alert({ }
message: __('E-Way Bill Cancelled successfully'),
indicator: 'green'
}, 7);
frm.reload_doc();
d.hide();
},
error: () => {
frappe.show_alert({
message: __('E-Way Bill was not Cancelled'),
indicator: 'red'
}, 7);
d.hide();
}
});
}, },
primary_action_label: __('Submit') primary_action_label: __('Yes')
}); });
d.show();
}; };
add_custom_button(__("Cancel E-Way Bill"), action); add_custom_button(__("Cancel E-Way Bill"), action);
} }

View File

@ -649,6 +649,8 @@ def make_einvoice(invoice):
try: try:
einvoice = safe_json_load(einvoice) einvoice = safe_json_load(einvoice)
einvoice = santize_einvoice_fields(einvoice) einvoice = santize_einvoice_fields(einvoice)
except json.JSONDecodeError:
raise
except Exception: except Exception:
show_link_to_error_log(invoice, einvoice) show_link_to_error_log(invoice, einvoice)
@ -765,7 +767,9 @@ def safe_json_load(json_string):
frappe.throw( frappe.throw(
_( _(
"Error in input data. Please check for any special characters near following input: <br> {}" "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.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
self.cancel_ewaybill_url = self.base_url + "/enriched/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.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
@ -1185,6 +1190,7 @@ class GSPConnector:
headers = self.get_headers() headers = self.get_headers()
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"] headers["username"] = headers["user_name"]
del headers["user_name"]
try: try:
res = self.make_request("post", self.cancel_ewaybill_url, headers, data) res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
if res.get("success"): if res.get("success"):
@ -1358,9 +1364,13 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist() @frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): def cancel_eway_bill(doctype, docname):
gsp_connector = GSPConnector(doctype, docname) # NOTE: cancel_eway_bill api is disabled by Adequare.
gsp_connector.cancel_eway_bill(eway_bill, reason, remark) # 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() @frappe.whitelist()

View File

@ -22,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
'shipping_address': frm.doc.shipping_address || '', 'shipping_address': frm.doc.shipping_address || '',
'shipping_address_name': frm.doc.shipping_address_name || '', 'shipping_address_name': frm.doc.shipping_address_name || '',
'customer_address': frm.doc.customer_address || '', 'customer_address': frm.doc.customer_address || '',
'company_address': frm.doc.company_address,
'supplier_address': frm.doc.supplier_address, 'supplier_address': frm.doc.supplier_address,
'customer': frm.doc.customer, 'customer': frm.doc.customer,
'supplier': frm.doc.supplier, 'supplier': frm.doc.supplier,

View File

@ -840,6 +840,30 @@ def get_gst_accounts(
return 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): def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country") country = frappe.get_cached_value("Company", doc.company, "country")
@ -887,6 +911,8 @@ def validate_reverse_charge_transaction(doc, method):
frappe.throw(msg) frappe.throw(msg)
doc.eligibility_for_itc = "ITC on Reverse Charge"
def update_itc_availed_fields(doc, method): def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country") country = frappe.get_cached_value("Company", doc.company, "country")

View File

@ -226,7 +226,10 @@ class Gstr1Report(object):
taxable_value += abs(net_amount) taxable_value += abs(net_amount)
elif ( elif (
not tax_rate 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" and invoice_details.get("export_type") == "Without Payment of Tax"
): ):
taxable_value += abs(net_amount) taxable_value += abs(net_amount)
@ -328,12 +331,14 @@ class Gstr1Report(object):
def get_invoice_items(self): def get_invoice_items(self):
self.invoice_items = frappe._dict() self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict()
self.item_hsn_map = frappe._dict()
self.nil_exempt_non_gst = {} self.nil_exempt_non_gst = {}
# nosemgrep
items = frappe.db.sql( items = frappe.db.sql(
""" """
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, 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) where parent in (%s)
""" """
% (self.doctype, ", ".join(["%s"] * len(self.invoices))), % (self.doctype, ", ".join(["%s"] * len(self.invoices))),
@ -343,6 +348,7 @@ class Gstr1Report(object):
for d in items: for d in items:
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) 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( self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
"base_net_amount", 0 "base_net_amount", 0
) )
@ -367,6 +373,8 @@ class Gstr1Report(object):
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0) self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
def get_items_based_on_tax_rate(self): def get_items_based_on_tax_rate(self):
hsn_wise_tax_rate = get_hsn_wise_tax_rates()
self.tax_details = frappe.db.sql( self.tax_details = frappe.db.sql(
""" """
select select
@ -427,7 +435,7 @@ class Gstr1Report(object):
alert=True, 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(): for invoice, items in self.invoice_items.items():
if ( if (
invoice not in self.items_based_on_tax_rate 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("export_type") == "Without Payment of Tax"
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ") 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): def get_columns(self):
self.other_columns = [] self.other_columns = []
@ -728,7 +746,7 @@ def get_json(filters, report_name, data):
elif filters["type_of_business"] == "EXPORT": elif filters["type_of_business"] == "EXPORT":
for item in report_data[:-1]: 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) out = get_export_json(res)
gst_json["exp"] = out gst_json["exp"] = out
@ -918,11 +936,21 @@ def get_export_json(res):
for exp_type in res: for exp_type in res:
exp_item, inv = {"exp_typ": exp_type, "inv": []}, [] exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
for row in res[exp_type]: for number, invoice in res[exp_type].items():
inv_item = get_basic_invoice_detail(row) inv_item = get_basic_invoice_detail(invoice[0])
inv_item["itms"] = [ inv_item["itms"] = []
{"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0}
] 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) inv.append(inv_item)
@ -1060,7 +1088,6 @@ def get_rate_and_tax_details(row, gstin):
# calculate tax amount added # calculate tax amount added
tax = flt((row["taxable_value"] * rate) / 100.0, 2) 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]: 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)}) itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
else: else:
@ -1136,3 +1163,26 @@ def get_company_gstins(company):
address_list = [""] + [d.gstin for d in addresses] address_list = [""] + [d.gstin for d in addresses]
return address_list 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

View File

@ -221,7 +221,7 @@ def get_merged_data(columns, data):
result = [] result = []
for row in data: for row in data:
key = row[0] + "-" + str(row[4]) key = row[0] + "-" + row[2] + "-" + str(row[4])
merged_hsn_dict.setdefault(key, {}) merged_hsn_dict.setdefault(key, {})
for i, d in enumerate(columns): for i, d in enumerate(columns):
if d["fieldtype"] not in ("Int", "Float", "Currency"): if d["fieldtype"] not in ("Int", "Float", "Currency"):

View File

@ -367,7 +367,14 @@ def set_credit_limit(customer, company, credit_limit):
customer.credit_limits[-1].db_insert() 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): if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc( customer = frappe.get_doc(
{ {

View File

@ -232,7 +232,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used") update_coupon_code_count(self.coupon_code, "used")
def on_cancel(self): 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() super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO # Cannot cancel closed SO

View File

@ -187,8 +187,9 @@ def get_so_with_invoices(filters):
.on(soi.parent == so.name) .on(soi.parent == so.name)
.join(ps) .join(ps)
.on(ps.parent == so.name) .on(ps.parent == so.name)
.select(so.name)
.distinct()
.select( .select(
so.name,
so.customer, so.customer,
so.transaction_date.as_("submitted"), so.transaction_date.as_("submitted"),
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),

View File

@ -64,7 +64,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
this.frm.set_query("item_code", "items", function() { this.frm.set_query("item_code", "items", function() {
return { return {
query: "erpnext.controllers.queries.item_query", 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}
} }
}); });
} }

View File

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

View File

@ -570,15 +570,12 @@ class TestDeliveryNote(FrappeTestCase):
customer=customer_name, customer=customer_name,
cost_center="Main - TCP1", cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1", expense_account="Cost of Goods Sold - TCP1",
do_not_submit=True,
qty=5, qty=5,
rate=500, rate=500,
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
target_warehouse=target_warehouse, target_warehouse=target_warehouse,
) )
dn.submit()
# qty after delivery # qty after delivery
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1") actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
self.assertEqual(actual_qty_at_source, 475) 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.items[0].returned_qty, 0)
self.assertEqual(dn2.per_billed, 100) 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): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@ -586,8 +586,7 @@ $.extend(erpnext.item, {
["parent","=", d.attribute] ["parent","=", d.attribute]
], ],
fields: ["attribute_value"], fields: ["attribute_value"],
limit_start: 0, limit_page_length: 0,
limit_page_length: 500,
parent: "Item Attribute", parent: "Item Attribute",
order_by: "idx" order_by: "idx"
} }

View File

@ -23,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"
class MaterialRequest(BuyingController): class MaterialRequest(BuyingController):
def get_feed(self): def get_feed(self):
return _("{0}: {1}").format(self.status, self.material_request_type) return
def check_if_already_pulled(self): def check_if_already_pulled(self):
pass pass

View File

@ -1285,6 +1285,14 @@ class TestPurchaseReceipt(FrappeTestCase):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
make_purchase_invoice as create_purchase_invoice, 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( pi = create_purchase_invoice(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
@ -1293,6 +1301,7 @@ class TestPurchaseReceipt(FrappeTestCase):
expense_account="_Test Account Cost for Goods Sold - TCP1", expense_account="_Test Account Cost for Goods Sold - TCP1",
currency="USD", currency="USD",
conversion_rate=70, conversion_rate=70,
supplier="_Test Supplier USD",
) )
pr = create_purchase_receipt(pi.name) pr = create_purchase_receipt(pi.name)

View File

@ -3,9 +3,11 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext import erpnext
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers 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, repost_future_sle,
) )
RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError)
class RepostItemValuation(Document): class RepostItemValuation(Document):
def validate(self): def validate(self):
@ -132,7 +136,7 @@ def repost(doc):
doc.set_status("Completed") doc.set_status("Completed")
except Exception: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
doc.log_error("Unable to repost item valuation") doc.log_error("Unable to repost item valuation")
@ -142,9 +146,9 @@ def repost(doc):
message += "<br>" + "Traceback: <br>" + traceback message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, "error_log", message) frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
notify_error_to_stock_managers(doc, message) if not isinstance(e, RecoverableErrors):
doc.set_status("Failed") notify_error_to_stock_managers(doc, message)
raise doc.set_status("Failed")
finally: finally:
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()

View File

@ -298,19 +298,17 @@ class StockEntry(StockController):
for_update=True, for_update=True,
) )
for f in ( reset_fields = ("stock_uom", "item_name")
"uom", for field in reset_fields:
"stock_uom", item.set(field, item_details.get(field))
"description",
"item_name", update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
"expense_account",
"cost_center", for field in update_fields:
"conversion_factor", if not item.get(field):
): item.set(field, item_details.get(field))
if f == "stock_uom" or not item.get(f): if field == "conversion_factor" and item.uom == item_details.get("stock_uom"):
item.set(f, item_details.get(f)) item.set(field, item_details.get(field))
if f == "conversion_factor" and item.uom == item_details.get("stock_uom"):
item.set(f, item_details.get(f))
if not item.transfer_qty and item.qty: if not item.transfer_qty and item.qty:
item.transfer_qty = flt( item.transfer_qty = flt(
@ -672,7 +670,8 @@ class StockEntry(StockController):
batch_no=d.batch_no, 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: if d.is_process_loss:
d.basic_rate = flt(0.0) d.basic_rate = flt(0.0)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) 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]) 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) 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]) 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 # Get raw materials cost from BOM if multiple material consumption entries
@ -760,10 +759,8 @@ class StockEntry(StockController):
for d in self.get("items"): for d in self.get("items"):
if d.transfer_qty: if d.transfer_qty:
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount")) d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
d.valuation_rate = flt( # Do not round off valuation rate to avoid precision loss
flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), d.valuation_rate = flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty))
d.precision("valuation_rate"),
)
def set_total_incoming_outgoing_value(self): def set_total_incoming_outgoing_value(self):
self.total_incoming_value = self.total_outgoing_value = 0.0 self.total_incoming_value = self.total_outgoing_value = 0.0
@ -1142,7 +1139,7 @@ class StockEntry(StockController):
if self.job_card: if self.job_card:
job_doc = frappe.get_doc("Job Card", 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(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: if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order) pro_doc = frappe.get_doc("Work Order", self.work_order)

View File

@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.permissions import add_user_permission, remove_user_permission from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings 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.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.item.test_item import (
create_item, create_item,
make_item,
make_item_variant, make_item_variant,
set_item_variant_settings, 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_rate, 100)
self.assertEqual(mapped_se.items[0].basic_amount, 200) 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): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -8,9 +8,8 @@ import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.query_builder.functions import CombineDatetime from frappe.query_builder.functions import CombineDatetime
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today from frappe.utils import add_days, add_to_date, flt, today
from frappe.utils.data import add_to_date
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs 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 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@ -1219,6 +1218,41 @@ class TestStockLedgerEntry(FrappeTestCase):
except Exception as e: except Exception as e:
self.fail("Double processing of qty for clashing timestamp.") 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): def create_repack_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -199,7 +199,7 @@ def process_args(args):
if not args.get("price_list"): if not args.get("price_list"):
args.price_list = args.get("selling_price_list") or args.get("buying_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) args.item_code = get_item_code(barcode=args.barcode)
elif not args.item_code and args.serial_no: elif not args.item_code and args.serial_no:
args.item_code = get_item_code(serial_no=args.serial_no) args.item_code = get_item_code(serial_no=args.serial_no)

View File

@ -252,11 +252,14 @@ def notify_errors(exceptions_list):
) )
for exception in exceptions_list: for exception in exceptions_list:
exception = json.loads(exception) try:
error_message = """<div class='small text-muted'>{0}</div><br>""".format( exception = json.loads(exception)
_(exception.get("message")) error_message = """<div class='small text-muted'>{0}</div><br>""".format(
) _(exception.get("message"))
content += error_message )
content += error_message
except Exception:
pass
content += _("Regards,") + "<br>" + _("Administrator") content += _("Regards,") + "<br>" + _("Administrator")

View File

@ -1,24 +1,29 @@
<h4>{{_("Request for Quotation")}}</h4> <h4>{{_("Request for Quotation")}}</h4>
<p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p> <p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p>
<p>{{ message }}</p> <p>{{ message }}</p>
<p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p> <p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p>
<p> <br>
<button style="border: 1px solid #15c; padding: 6px; border-radius: 5px; background-color: white;"> <a
<a href="{{ rfq_link }}" style="color: #15c; text-decoration:none;" target="_blank">Submit your Quotation</a> href="{{ rfq_link }}"
</button> class="btn btn-default btn-sm"
</p><br> target="_blank">
{{ _("Submit your Quotation") }}
<p>{{_("Regards")}},<br> </a>
{{ user_fullname }}</p><br> <br>
<br>
{% if update_password_link %} {% if update_password_link %}
<br>
<p>{{_("Please click on the following button to set your new password")}}:</p> <p>{{_("Please click on the following button to set your new password")}}:</p>
<p> <a
<button style="border: 1px solid #15c; padding: 4px; border-radius: 5px; background-color: white;"> href="{{ update_password_link }}"
<a href="{{ update_password_link }}" style="color: #15c; font-size: 12px; text-decoration:none;" target="_blank">{{_("Update Password") }}</a> class="btn btn-default btn-xs"
</button> target="_blank">
</p> {{_("Set Password") }}
</a>
<br>
<br>
{% endif %} {% endif %}
<p>
{{_("Regards")}},<br>
{{ user_fullname }}
</p>

View File

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

View File

@ -8,6 +8,7 @@ class TestSearch(unittest.TestCase):
# Search for the word "cond", part of the word "conduire" (Lead) in french. # Search for the word "cond", part of the word "conduire" (Lead) in french.
def test_contact_search_in_foreign_language(self): def test_contact_search_in_foreign_language(self):
try: try:
frappe.local.lang_full_dict = None # reset cached translations
frappe.local.lang = "fr" frappe.local.lang = "fr"
output = filter_dynamic_link_doctypes( output = filter_dynamic_link_doctypes(
"DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"}

View File

@ -1701,7 +1701,7 @@ No Permission,Keine Berechtigung,
No Remarks,Keine Anmerkungen, No Remarks,Keine Anmerkungen,
No Result to submit,Kein Ergebnis zur Einreichung, 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 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 Student Groups created.,Keine Studentengruppen erstellt.,
No Students in,Keine Studenten in, 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., 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 Category first,Bitte zuerst Kategorie auswählen,
Please select Charge Type first,Bitte zuerst Chargentyp auswählen, Please select Charge Type first,Bitte zuerst Chargentyp auswählen,
Please select Company,Bitte Unternehmen 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 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 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, 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 Batch,Split Batch,
Split Issue,Split-Problem, Split Issue,Split-Problem,
Sports,Sport, 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,Standard,
Standard Buying,Standard-Kauf, Standard Buying,Standard-Kauf,
Standard Selling,Standard-Vertrieb, Standard Selling,Standard-Vertrieb,
@ -3710,7 +3710,7 @@ Delivered Quantity,Gelieferte Menge,
Delivery Notes,Lieferscheine, Delivery Notes,Lieferscheine,
Depreciated Amount,Abschreibungsbetrag, Depreciated Amount,Abschreibungsbetrag,
Description,Beschreibung, Description,Beschreibung,
Designation,Bezeichnung, Designation,Position,
Difference Value,Differenzwert, Difference Value,Differenzwert,
Dimension Filter,Dimensionsfilter, Dimension Filter,Dimensionsfilter,
Disabled,Deaktiviert, 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 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 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 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 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 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", 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, Rejected Qty,Abgelehnt Menge,
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor, UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
Discount on Price List Rate (%),Rabatt auf die Preisliste (%), Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
Price List Rate (Company Currency),Preisliste (Unternehmenswährung), Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
Rate ,Preis, Rate ,Preis,
Rate (Company Currency),Preis (Unternehmenswährung), Rate (Company Currency),Preis (Unternehmenswährung),
Amount (Company Currency),Betrag (Unternehmenswährung), Amount (Company Currency),Betrag (Unternehmenswährung),
@ -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, 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", 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, 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, 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 &quot;Genehmigt&quot; haben.", 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 &quot;Genehmigt&quot; haben.",
Custom Signature in Print,Kundenspezifische Unterschrift im Druck, Custom Signature in Print,Kundenspezifische Unterschrift im Druck,
@ -6499,7 +6499,7 @@ Department Approver,Abteilungsgenehmiger,
Approver,Genehmiger, Approver,Genehmiger,
Required Skills,Benötigte Fähigkeiten, Required Skills,Benötigte Fähigkeiten,
Skills,Kompetenzen, Skills,Kompetenzen,
Designation Skill,Bezeichnung Fähigkeit, Designation Skill,Positions Fähigkeit,
Skill,Fertigkeit, Skill,Fertigkeit,
Driver,Fahrer/-in, Driver,Fahrer/-in,
HR-DRI-.YYYY.-,HR-DRI-.YYYY.-, HR-DRI-.YYYY.-,HR-DRI-.YYYY.-,
@ -6517,20 +6517,20 @@ Driver licence class,Führerscheinklasse,
HR-EMP-,HR-EMP-, HR-EMP-,HR-EMP-,
Employment Type,Art der Beschäftigung, Employment Type,Art der Beschäftigung,
Emergency Contact,Notfallkontakt, Emergency Contact,Notfallkontakt,
Emergency Contact Name,Notfall Kontaktname, Emergency Contact Name,Name des Notfallkontakts,
Emergency Phone,Notruf, Emergency Phone,Telefonnummer des Notfallkontakts,
ERPNext User,ERPNext Benutzer, ERPNext User,ERPNext Benutzer,
"System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.", "System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.",
Create User Permission,Benutzerberechtigung Erstellen, Create User Permission,Benutzerberechtigung Erstellen,
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein, This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
Joining Details,Details des Beitritts, Joining Details,Details des Beitritts,
Offer Date,Angebotsdatum, Offer Date,Angebotsdatum,
Confirmation Date,Datum bestätigen, Confirmation Date,Bestätigungsdatum,
Contract End Date,Vertragsende, Contract End Date,Vertragsende,
Notice (days),Meldung(s)(-Tage), Notice (days),Kündigungsfrist (Tage),
Date Of Retirement,Zeitpunkt der Pensionierung, Date Of Retirement,Zeitpunkt der Pensionierung,
Department and Grade,Abteilung und Klasse, Department and Grade,Abteilung und Klasse,
Reports to,Berichte an, Reports to,Vorgesetzter,
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
Leave Policy,Urlaubsrichtlinie, Leave Policy,Urlaubsrichtlinie,
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
@ -6553,8 +6553,8 @@ Company Email,E-Mail-Adresse des Unternehmens,
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert, Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
Current Address Is,Aktuelle Adresse ist, Current Address Is,Aktuelle Adresse ist,
Current Address,Aktuelle Adresse, Current Address,Aktuelle Adresse,
Personal Bio,Persönliches Bio, Personal Bio,Lebenslauf,
Bio / Cover Letter,Bio / Anschreiben, Bio / Cover Letter,Lebenslauf / Anschreiben,
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen., Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
Passport Number,Passnummer, Passport Number,Passnummer,
Date of Issue,Ausstellungsdatum, Date of Issue,Ausstellungsdatum,
@ -6798,7 +6798,7 @@ Select Employees,Mitarbeiter auswählen,
Employment Type (optional),Anstellungsart (optional), Employment Type (optional),Anstellungsart (optional),
Branch (optional),Zweigstelle (optional), Branch (optional),Zweigstelle (optional),
Department (optional),Abteilung (optional), Department (optional),Abteilung (optional),
Designation (optional),Bezeichnung (optional), Designation (optional),Position (optional),
Employee Grade (optional),Dienstgrad (optional), Employee Grade (optional),Dienstgrad (optional),
Employee (optional),Mitarbeiter (optional), Employee (optional),Mitarbeiter (optional),
Allocate Leaves,Blätter zuweisen, Allocate Leaves,Blätter zuweisen,
@ -7769,7 +7769,7 @@ Authorized Value,Autorisierter Wert,
Applicable To (Role),Anwenden auf (Rolle), Applicable To (Role),Anwenden auf (Rolle),
Applicable To (Employee),Anwenden auf (Mitarbeiter), Applicable To (Employee),Anwenden auf (Mitarbeiter),
Applicable To (User),Anwenden auf (Benutzer), 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 Role (above authorized value),Genehmigende Rolle (über dem autorisierten Wert),
Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert), Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert),
Brand Defaults,Markenstandards, Brand Defaults,Markenstandards,
@ -8946,7 +8946,7 @@ Requesting Practitioner,Praktizierender anfordern,
Requesting Department,Abteilung anfordern, Requesting Department,Abteilung anfordern,
Employee (Lab Technician),Mitarbeiter (Labortechniker), Employee (Lab Technician),Mitarbeiter (Labortechniker),
Lab Technician Name,Name des Labortechnikers, Lab Technician Name,Name des Labortechnikers,
Lab Technician Designation,Bezeichnung des Labortechnikers, Lab Technician Designation,Position des Labortechnikers,
Compound Test Result,Zusammengesetztes Testergebnis, Compound Test Result,Zusammengesetztes Testergebnis,
Organism Test Result,Organismustestergebnis, Organism Test Result,Organismustestergebnis,
Sensitivity Test Result,Empfindlichkeitstestergebnis, Sensitivity Test Result,Empfindlichkeitstestergebnis,
@ -9542,7 +9542,7 @@ Preview Email,Vorschau E-Mail,
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus, Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage), Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
"Home, Work, etc.","Zuhause, Arbeit usw.", "Home, Work, etc.","Zuhause, Arbeit usw.",
Exit Interview Held On,Beenden Sie das Interview, Exit Interview Held On,Entlassungsgespräch am,
Condition and formula,Zustand und Formel, Condition and formula,Zustand und Formel,
Sets 'Target Warehouse' in each row of the Items table.,Legt &#39;Ziellager&#39; in jeder Zeile der Elementtabelle fest., Sets 'Target Warehouse' in each row of the Items table.,Legt &#39;Ziellager&#39; in jeder Zeile der Elementtabelle fest.,
Sets 'Source Warehouse' in each row of the Items table.,Legt &#39;Source Warehouse&#39; in jeder Zeile der Items-Tabelle fest., Sets 'Source Warehouse' in each row of the Items table.,Legt &#39;Source Warehouse&#39; in jeder Zeile der Items-Tabelle fest.,

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

File diff suppressed because it is too large Load Diff