Merge branch 'develop' into fix-average-discount-auth

This commit is contained in:
Deepesh Garg 2022-05-26 18:22:34 +05:30 committed by GitHub
commit d0a9eb4fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 147029 additions and 1279 deletions

View File

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

View File

@ -94,7 +94,7 @@ class JournalEntry(AccountsController):
unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()

View File

@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()

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

View File

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

View File

@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase):
title="_Test Pricing Rule with Min Qty - 2",
)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
item = si.items[0]
item.stock_qty = 1
si.save()

View File

@ -1418,7 +1418,12 @@ class PurchaseInvoice(BuyingController):
frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Payment Ledger Entry",
)
self.update_advance_tax_references(cancel=1)
def update_project(self):

View File

@ -1,10 +1,12 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"title",
"is_default",
@ -74,7 +76,8 @@
],
"icon": "fa fa-money",
"idx": 1,
"modified": "2019-11-25 13:05:26.220275",
"links": [],
"modified": "2022-05-16 16:15:29.059370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
@ -103,6 +106,10 @@
"role": "Purchase User"
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@ -861,27 +861,44 @@ frappe.ui.form.on('Sales Invoice', {
set_timesheet_data: function(frm, timesheets) {
frm.clear_table("timesheets")
timesheets.forEach(timesheet => {
timesheets.forEach(async (timesheet) => {
if (frm.doc.currency != timesheet.currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: timesheet.currency,
to_currency: frm.doc.currency
},
callback: function(r) {
if (r.message) {
exchange_rate = r.message;
frm.events.append_time_log(frm, timesheet, exchange_rate);
}
}
});
const exchange_rate = await frm.events.get_exchange_rate(
frm, timesheet.currency, frm.doc.currency
)
frm.events.append_time_log(frm, timesheet, exchange_rate)
} else {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
},
async get_exchange_rate(frm, from_currency, to_currency) {
if (
frm.exchange_rates
&& frm.exchange_rates[from_currency]
&& frm.exchange_rates[from_currency][to_currency]
) {
return frm.exchange_rates[from_currency][to_currency];
}
return frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency,
to_currency
},
callback: function(r) {
if (r.message) {
// cache exchange rates
frm.exchange_rates = frm.exchange_rates || {};
frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {};
frm.exchange_rates[from_currency][to_currency] = r.message;
}
}
});
},
append_time_log: function(frm, time_log, exchange_rate) {
const row = frm.add_child("timesheets");
row.activity_type = time_log.activity_type;
@ -892,7 +909,7 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_hours = time_log.billing_hours;
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name;
row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");

View File

@ -396,7 +396,12 @@ class SalesInvoice(SellingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Payment Ledger Entry",
)
def update_status_updater_args(self):
if cint(self.update_stock):

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
@ -77,7 +78,8 @@
],
"icon": "fa fa-money",
"idx": 1,
"modified": "2019-11-25 13:06:03.279099",
"links": [],
"modified": "2022-05-16 16:14:52.061672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
@ -113,7 +115,10 @@
"write": 1
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError):
@ -34,6 +35,7 @@ def make_gl_entries(
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
create_payment_ledger_entry(gl_map)
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
@ -479,6 +481,7 @@ def make_reverse_gl_entries(
).run(as_dict=1)
if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])

View File

@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
return None
else:
return None
def add_party_account(party_type, party, company, account):
doc = frappe.get_doc(party_type, party)
account_exists = False
for d in doc.get("accounts"):
if d.account == account:
account_exists = True
if not account_exists:
accounts = {"company": company, "account": account}
doc.append("accounts", accounts)
doc.save()

View File

@ -198,10 +198,12 @@ def get_loan_entries(filters):
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
salary_condition = loan_doc.docstatus == 1
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
salary_condition = loan_doc.repay_from_salary == 0
query = (
frappe.qb.from_(loan_doc)
@ -214,14 +216,12 @@ def get_loan_entries(filters):
posting_date,
)
.where(loan_doc.docstatus == 1)
.where(salary_condition)
.where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
)
if doctype == "Loan Repayment":
query.where(loan_doc.repay_from_salary == 0)
entries = query.run(as_dict=1)
loan_docs.extend(entries)
@ -267,15 +267,17 @@ def get_loan_amount(filters):
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
salary_condition = loan_doc.docstatus == 1
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
salary_condition = loan_doc.repay_from_salary == 0
amount = (
frappe.qb.from_(loan_doc)
.select(amount_field)
.where(loan_doc.docstatus == 1)
.where(salary_condition)
.where(account == filters.get("account"))
.where(posting_date > getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))

View File

@ -262,7 +262,10 @@ def get_report_summary(summary_data, currency):
def get_chart_data(columns, data):
labels = [d.get("label") for d in columns[2:]]
datasets = [
{"name": account.get("account").replace("'", ""), "values": [account.get("total")]}
{
"name": account.get("account").replace("'", ""),
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
}
for account in data
if account.get("parent_account") == None and account.get("currency")
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from typing import List, Tuple
import frappe
import frappe.defaults
from frappe import _, throw
from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
@ -15,6 +15,7 @@ import erpnext
# imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency # noqa
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report):
if icons:
for icon in icons:
frappe.delete_doc("Desktop Icon", icon)
def create_payment_ledger_entry(gl_entries, cancel=0):
if gl_entries:
ple = None
# companies
account = qb.DocType("Account")
companies = list(set([x.company for x in gl_entries]))
# receivable/payable account
accounts_with_types = (
qb.from_(account)
.select(account.name, account.account_type)
.where(
(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
)
.run(as_dict=True)
)
receivable_or_payable_accounts = [y.name for y in accounts_with_types]
def get_account_type(account):
for entry in accounts_with_types:
if entry.name == account:
return entry.account_type
dr_or_cr = 0
account_type = None
for gle in gl_entries:
if gle.account in receivable_or_payable_accounts:
account_type = get_account_type(gle.account)
if account_type == "Receivable":
dr_or_cr = gle.debit - gle.credit
dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
elif account_type == "Payable":
dr_or_cr = gle.credit - gle.debit
dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
if cancel:
dr_or_cr *= -1
dr_or_cr_account_currency *= -1
ple = frappe.get_doc(
{
"doctype": "Payment Ledger Entry",
"posting_date": gle.posting_date,
"company": gle.company,
"account_type": account_type,
"account": gle.account,
"party_type": gle.party_type,
"party": gle.party,
"cost_center": gle.cost_center,
"finance_book": gle.finance_book,
"due_date": gle.due_date,
"voucher_type": gle.voucher_type,
"voucher_no": gle.voucher_no,
"against_voucher_type": gle.against_voucher_type
if gle.against_voucher_type
else gle.voucher_type,
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
"currency": gle.currency,
"amount": dr_or_cr,
"amount_in_account_currency": dr_or_cr_account_currency,
"delinked": True if cancel else False,
}
)
dimensions_and_defaults = get_dimensions()
if dimensions_and_defaults:
for dimension in dimensions_and_defaults[0]:
ple.set(dimension.fieldname, gle.get(dimension.fieldname))
if cancel:
delink_original_entry(ple)
ple.flags.ignore_permissions = 1
ple.submit()
def delink_original_entry(pl_entry):
if pl_entry:
ple = qb.DocType("Payment Ledger Entry")
query = (
qb.update(ple)
.set(ple.delinked, True)
.set(ple.modified, now())
.set(ple.modified_by, frappe.session.user)
.where(
(ple.company == pl_entry.company)
& (ple.account_type == pl_entry.account_type)
& (ple.account == pl_entry.account)
& (ple.party_type == pl_entry.party_type)
& (ple.party == pl_entry.party)
& (ple.voucher_type == pl_entry.voucher_type)
& (ple.voucher_no == pl_entry.voucher_no)
& (ple.against_voucher_type == pl_entry.against_voucher_type)
& (ple.against_voucher_no == pl_entry.against_voucher_no)
)
)
query.run()

View File

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

View File

@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
from erpnext.accounts.party import (
get_party_account,
get_party_account_currency,
get_party_gle_currency,
validate_party_frozen_disabled,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
@ -148,6 +149,7 @@ class AccountsController(TransactionBase):
self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
self.set_incoming_rate()
if self.meta.get_field("currency"):
@ -167,6 +169,7 @@ class AccountsController(TransactionBase):
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
@ -382,6 +385,14 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
frappe.msgprint(
_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
alert=1,
)
def validate_due_date(self):
if self.get("is_pos"):
return
@ -1121,11 +1132,10 @@ class AccountsController(TransactionBase):
{
"account": item.discount_account,
"against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
dr_or_cr
+ "_in_account_currency": flt(
dr_or_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project,
},
@ -1140,11 +1150,11 @@ class AccountsController(TransactionBase):
{
"account": income_or_expense_account,
"against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
rev_dr_cr
+ "_in_account_currency": flt(
rev_dr_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
rev_dr_cr
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
@ -1439,6 +1449,27 @@ class AccountsController(TransactionBase):
# at quotation / sales order level and we shouldn't stop someone
# from creating a sales invoice if sales order is already created
def validate_party_account_currency(self):
if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
return
if self.is_opening == "Yes":
return
party_type, party = self.get_party()
party_gle_currency = get_party_gle_currency(party_type, party, self.company)
party_account = (
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
)
party_account_currency = get_account_currency(party_account)
if not party_gle_currency and (party_account_currency != self.currency):
frappe.throw(
_("Party Account {0} currency and document currency should be same").format(
frappe.bold(party_account)
)
)
def delink_advance_entries(self, linked_doc_name):
total_allocated_amount = 0
for adv in self.advances:
@ -1738,6 +1769,8 @@ class AccountsController(TransactionBase):
internal_party_field = "is_internal_customer"
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
internal_party_field = "is_internal_supplier"
else:
return False
if self.get(internal_party_field) and (self.represents_company == self.company):
return True

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,9 @@ class Attendance(Document):
self.validate_employee_status()
self.check_leave_record()
def on_cancel(self):
self.unlink_attendance_from_checkins()
def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
@ -127,6 +130,33 @@ class Attendance(Document):
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
def unlink_attendance_from_checkins(self):
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
linked_logs = (
frappe.qb.from_(EmployeeCheckin)
.select(EmployeeCheckin.name)
.where(EmployeeCheckin.attendance == self.name)
.for_update()
.run(as_dict=True)
)
if linked_logs:
(
frappe.qb.update(EmployeeCheckin)
.set("attendance", "")
.where(EmployeeCheckin.attendance == self.name)
).run()
frappe.msgprint(
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
),
title=_("Unlinked logs"),
indicator="blue",
is_minimizable=True,
wide=True,
)
def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
attendance = frappe.qb.DocType("Attendance")

View File

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

View File

@ -76,6 +76,17 @@ class TestEmployeeCheckin(FrappeTestCase):
)
self.assertEqual(attendance_count, 1)
def test_unlink_attendance_on_cancellation(self):
employee = make_employee("test_mark_attendance_and_link_log@example.com")
logs = make_n_checkins(employee, 3)
frappe.db.delete("Attendance", {"employee": employee})
attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2)
attendance.cancel()
linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name})
self.assertEquals(len(linked_logs), 0)
def test_calculate_working_hours(self):
check_in_out_type = [
"Alternating entries as IN and OUT during the same shift",
@ -125,6 +136,11 @@ class TestEmployeeCheckin(FrappeTestCase):
)
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
working_hours = calculate_working_hours(
[logs_type_2[1], logs_type_2[-1]], check_in_out_type[1], working_hours_calc_type[1]
)
self.assertEqual(working_hours, (5.0, logs_type_2[1].time, logs_type_2[-1].time))
def test_fetch_shift(self):
employee = make_employee("test_employee_checkin@example.com", company="_Test Company")

View File

@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
def on_cancel(self):
self.update_task_and_project()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
if self.payable_account:
self.make_gl_entries(cancel=True)

View File

@ -6,6 +6,7 @@
import frappe
from frappe import _
from frappe.utils import get_link_to_form
from frappe.website.website_generator import WebsiteGenerator
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator):
self.staffing_plan = staffing_plan[0].name
self.planned_vacancies = staffing_plan[0].vacancies
elif not self.planned_vacancies:
planned_vacancies = frappe.db.sql(
"""
select vacancies from `tabStaffing Plan Detail`
where parent=%s and designation=%s""",
(self.staffing_plan, self.designation),
self.planned_vacancies = frappe.db.get_value(
"Staffing Plan Detail",
{"parent": self.staffing_plan, "designation": self.designation},
"vacancies",
)
self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
if self.staffing_plan and self.planned_vacancies:
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
designation_counts = get_designation_counts(self.designation, self.company)
designation_counts = get_designation_counts(self.designation, self.company, self.name)
current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
if self.planned_vacancies <= current_count:
number_of_positions = frappe.db.get_value(
"Staffing Plan Detail",
{"parent": self.staffing_plan, "designation": self.designation},
"number_of_positions",
)
if number_of_positions <= current_count:
frappe.throw(
_(
"Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
).format(self.designation, self.staffing_plan)
"Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
).format(
frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
),
title=_("Vacancies fulfilled"),
)
def get_context(self, context):

View File

@ -3,8 +3,77 @@
import unittest
# test_records = frappe.get_test_records('Job Opening')
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
class TestJobOpening(unittest.TestCase):
pass
class TestJobOpening(FrappeTestCase):
def setUp(self):
frappe.db.delete("Staffing Plan")
frappe.db.delete("Staffing Plan Detail")
frappe.db.delete("Job Opening")
make_company("_Test Opening Company", "_TOC")
frappe.db.delete("Employee", {"company": "_Test Opening Company"})
def test_vacancies_fulfilled(self):
make_employee(
"test_job_opening@example.com", company="_Test Opening Company", designation="Designer"
)
staffing_plan = frappe.get_doc(
{
"doctype": "Staffing Plan",
"company": "_Test Opening Company",
"name": "Test",
"from_date": getdate(),
"to_date": add_days(getdate(), 10),
}
)
staffing_plan.append(
"staffing_details",
{"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
)
staffing_plan.insert()
staffing_plan.submit()
self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2)
# allows creating 1 job opening as per vacancy
opening_1 = get_job_opening()
opening_1.insert()
# vacancies as per staffing plan already fulfilled via job opening and existing employee count
opening_2 = get_job_opening(job_title="Designer New")
self.assertRaises(frappe.ValidationError, opening_2.insert)
# allows updating existing job opening
opening_1.status = "Closed"
opening_1.save()
def get_job_opening(**args):
args = frappe._dict(args)
opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"})
if opening:
return frappe.get_doc("Job Opening", opening)
opening = frappe.get_doc(
{
"doctype": "Job Opening",
"job_title": "Designer",
"designation": "Designer",
"company": "_Test Opening Company",
"status": "Open",
}
)
opening.update(args)
return opening

View File

@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
@ -107,7 +107,10 @@ class LeaveEncashment(Document):
self.leave_balance = (
allocation.total_leaves_allocated
- allocation.carry_forwarded_leaves_count
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
# adding this because the function returns a -ve number
+ get_leaves_for_period(
self.employee, self.leave_type, allocation.from_date, self.encashment_date
)
)
encashable_days = self.leave_balance - frappe.db.get_value(
@ -126,14 +129,25 @@ class LeaveEncashment(Document):
return True
def get_leave_allocation(self):
leave_allocation = frappe.db.sql(
"""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
between from_date and to_date and docstatus=1 and leave_type='{1}'
and employee= '{2}'""".format(
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
),
as_dict=1,
) # nosec
date = self.encashment_date or getdate()
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
leave_allocation = (
frappe.qb.from_(LeaveAllocation)
.select(
LeaveAllocation.name,
LeaveAllocation.from_date,
LeaveAllocation.to_date,
LeaveAllocation.total_leaves_allocated,
LeaveAllocation.carry_forwarded_leaves_count,
)
.where(
((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date))
& (LeaveAllocation.docstatus == 1)
& (LeaveAllocation.leave_type == self.leave_type)
& (LeaveAllocation.employee == self.employee)
)
).run(as_dict=True)
return leave_allocation[0] if leave_allocation else None

View File

@ -4,26 +4,42 @@
import unittest
import frappe
from frappe.utils import add_months, today
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
)
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
test_dependencies = ["Leave Type"]
test_records = frappe.get_test_records("Leave Type")
class TestLeaveEncashment(unittest.TestCase):
class TestLeaveEncashment(FrappeTestCase):
def setUp(self):
frappe.db.sql("""delete from `tabLeave Period`""")
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
frappe.db.sql("""delete from `tabLeave Allocation`""")
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
frappe.db.sql("""delete from `tabAdditional Salary`""")
frappe.db.delete("Leave Period")
frappe.db.delete("Leave Policy Assignment")
frappe.db.delete("Leave Allocation")
frappe.db.delete("Leave Ledger Entry")
frappe.db.delete("Additional Salary")
frappe.db.delete("Leave Encashment")
if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
frappe.get_doc(test_records[2]).insert()
date = getdate()
year_start = getdate(get_year_start(date))
year_end = getdate(get_year_ending(date))
make_holiday_list("_Test Leave Encashment", year_start, year_end)
# create the leave policy
leave_policy = create_leave_policy(
@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
leave_policy.submit()
# create employee, salary structure and assignment
self.employee = make_employee("test_employee_encashment@example.com")
self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company")
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
self.leave_period = create_leave_period(year_start, year_end, "_Test Company")
data = {
"assignment_based_on": "Leave Period",
@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
other_details={"leave_encashment_amount_per_day": 50},
)
def tearDown(self):
for dt in [
"Leave Period",
"Leave Allocation",
"Leave Ledger Entry",
"Additional Salary",
"Leave Encashment",
"Salary Structure",
"Leave Policy",
]:
frappe.db.sql("delete from `tab%s`" % dt)
@set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_leave_balance_value_and_amount(self):
frappe.db.sql("""delete from `tabLeave Encashment`""")
leave_encashment = frappe.get_doc(
dict(
doctype="Leave Encashment",
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today(),
encashment_date=self.leave_period.to_date,
currency="INR",
)
).insert()
@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
self.assertTrue(add_sal)
def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.sql("""delete from `tabLeave Encashment`""")
@set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_leave_balance_value_with_leaves_and_amount(self):
date = self.leave_period.from_date
leave_application = make_leave_application(
self.employee, date, add_days(date, 3), "_Test Leave Type Encashment"
)
leave_application.reload()
leave_encashment = frappe.get_doc(
dict(
doctype="Leave Encashment",
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today(),
encashment_date=self.leave_period.to_date,
currency="INR",
)
).insert()
self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days)
# encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1
# with charge of 50 per day
self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5)
self.assertEqual(leave_encashment.encashment_amount, 50)
leave_encashment.submit()
# assert links
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
self.assertTrue(add_sal)
@set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_creation_of_leave_ledger_entry_on_submit(self):
leave_encashment = frappe.get_doc(
dict(
doctype="Leave Encashment",
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
encashment_date=self.leave_period.to_date,
currency="INR",
)
).insert()

View File

@ -172,27 +172,24 @@ class StaffingPlan(Document):
@frappe.whitelist()
def get_designation_counts(designation, company):
def get_designation_counts(designation, company, job_opening=None):
if not designation:
return False
employee_counts = {}
company_set = get_descendants_of("Company", company)
company_set.append(company)
employee_counts["employee_count"] = frappe.db.get_value(
"Employee",
filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
fieldname=["count(name)"],
employee_count = frappe.db.count(
"Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
)
employee_counts["job_openings"] = frappe.db.get_value(
"Job Opening",
filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
fieldname=["count(name)"],
)
filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
if job_opening:
filters["name"] = ("!=", job_opening)
return employee_counts
job_openings = frappe.db.count("Job Opening", filters)
return {"employee_count": employee_count, "job_openings": job_openings}
@frappe.whitelist()

View File

@ -85,13 +85,16 @@ def _set_up():
make_company()
def make_company():
if frappe.db.exists("Company", "_Test Company 10"):
def make_company(name=None, abbr=None):
if not name:
name = "_Test Company 10"
if frappe.db.exists("Company", name):
return
company = frappe.new_doc("Company")
company.company_name = "_Test Company 10"
company.abbr = "_TC10"
company.company_name = name
company.abbr = abbr or "_TC10"
company.parent_company = "_Test Company 3"
company.default_currency = "INR"
company.country = "Pakistan"

View File

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

View File

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

View File

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

View File

@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
pass
class JobCardOverTransferError(frappe.ValidationError):
pass
class JobCard(Document):
def onload(self):
excess_transfer = frappe.db.get_single_value(
@ -522,23 +526,50 @@ class JobCard(Document):
},
)
def set_transferred_qty_in_job_card(self, ste_doc):
def set_transferred_qty_in_job_card_item(self, ste_doc):
from frappe.query_builder.functions import Sum
def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings."
required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty")
is_excess = flt(transferred_qty) > flt(required_qty)
if is_excess:
frappe.throw(
_(
"Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
).format(
row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card
),
title=_("Excess Transfer"),
exc=JobCardOverTransferError,
)
for row in ste_doc.items:
if not row.job_card_item:
continue
qty = frappe.db.sql(
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
se.purpose = 'Material Transfer for Manufacture'
""",
(row.job_card_item),
)[0][0]
sed = frappe.qb.DocType("Stock Entry Detail")
se = frappe.qb.DocType("Stock Entry")
transferred_qty = (
frappe.qb.from_(sed)
.join(se)
.on(sed.parent == se.name)
.select(Sum(sed.qty))
.where(
(sed.job_card_item == row.job_card_item)
& (se.docstatus == 1)
& (se.purpose == "Material Transfer for Manufacture")
)
).run()[0][0]
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
if not allow_excess:
_validate_over_transfer(row, transferred_qty)
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty for which RM was transferred."
"Set total FG Qty in Job Card for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@ -866,6 +897,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.set("time_logs", [])
target.set("employee", [])
target.set("items", [])
target.set("sub_operations", [])
target.set_sub_operations()
target.get_required_items()
target.validate_time_logs()

View File

@ -1,15 +1,25 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
from typing import Literal
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import random_string
from frappe.utils.data import add_to_date, now
from erpnext.manufacturing.doctype.job_card.job_card import (
JobCardOverTransferError,
OperationMismatchError,
OverlapError,
make_corrective_job_card,
)
from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc,
)
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -17,34 +27,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestJobCard(FrappeTestCase):
def setUp(self):
make_bom_for_jc_tests()
self.transfer_material_against: Literal["Work Order", "Job Card"] = "Work Order"
self.source_warehouse = None
self._work_order = None
transfer_material_against, source_warehouse = None, None
@property
def work_order(self) -> WorkOrder:
"""Work Order lazily created for tests."""
if not self._work_order:
self._work_order = make_wo_order_test_record(
item="_Test FG Item 2",
qty=2,
transfer_material_against=self.transfer_material_against,
source_warehouse=self.source_warehouse,
)
return self._work_order
tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
tests_that_transfer_against_jc = (
"test_job_card_multiple_materials_transfer",
"test_job_card_excess_material_transfer",
"test_job_card_partial_material_transfer",
)
if self._testMethodName in tests_that_skip_setup:
return
if self._testMethodName in tests_that_transfer_against_jc:
transfer_material_against = "Job Card"
source_warehouse = "Stores - _TC"
self.work_order = make_wo_order_test_record(
item="_Test FG Item 2",
qty=2,
transfer_material_against=transfer_material_against,
source_warehouse=source_warehouse,
)
def generate_required_stock(self, work_order: WorkOrder) -> None:
"""Create twice the stock for all required items in work order."""
for item in work_order.required_items:
make_stock_entry(
item_code=item.item_code,
target=item.source_warehouse or self.source_warehouse,
qty=item.required_qty * 2,
basic_rate=100,
)
def tearDown(self):
frappe.db.rollback()
def test_job_card(self):
def test_job_card_operations(self):
job_cards = frappe.get_all(
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
@ -58,9 +70,6 @@ class TestJobCard(FrappeTestCase):
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
job_cards = frappe.get_all(
"Job Card",
@ -96,19 +105,11 @@ class TestJobCard(FrappeTestCase):
)
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
employee = "_T-Employee-00001" # from test records
@ -137,10 +138,10 @@ class TestJobCard(FrappeTestCase):
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
)
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
@ -165,16 +166,58 @@ class TestJobCard(FrappeTestCase):
# transfer was made for 2 fg qty in first transfer Stock Entry
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
self.generate_required_stock(self.work_order)
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card.name)
transfer_entry_1.insert()
transfer_entry_1.submit()
# transfer extra qty of both RM due to previously damaged RM
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
# deliberately change 'For Quantity'
transfer_entry_2.fg_completed_qty = 1
transfer_entry_2.items[0].qty = 5
transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert()
transfer_entry_2.submit()
job_card.reload()
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
# Check if 'For Quantity' is negative
# as 'transferred_qty' > Qty to Manufacture
transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append(
"time_logs",
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
)
job_card.save()
job_card.submit()
# JC is Completed with excess transfer
self.assertEqual(job_card.status, "Completed")
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
def test_job_card_excess_material_transfer_block(self):
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
@ -188,39 +231,19 @@ class TestJobCard(FrappeTestCase):
transfer_entry_2.items[0].qty = 5
transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert()
transfer_entry_2.submit()
job_card.reload()
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
# Check if 'For Quantity' is negative
# as 'transferred_qty' > Qty to Manufacture
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append(
"time_logs",
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
)
job_card.save()
job_card.submit()
# JC is Completed with excess transfer
self.assertEqual(job_card.status, "Completed")
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card"
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
)
self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
# partially transfer
transfer_entry = make_stock_entry_from_jc(job_card_name)
transfer_entry = make_stock_entry_from_jc(job_card.name)
transfer_entry.fg_completed_qty = 1
transfer_entry.get_items()
transfer_entry.insert()
@ -232,7 +255,7 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(transfer_entry.items[1].qty, 3)
# transfer remaining
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
self.assertEqual(transfer_entry_2.items[0].qty, 5)
@ -277,7 +300,49 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
self.assertEqual(transfer_entry.items[0].qty, 2)
# rollback via tearDown method
@change_settings(
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
)
def test_corrective_costing(self):
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
job_card.append(
"time_logs",
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
)
job_card.submit()
self.work_order.reload()
original_cost = self.work_order.total_operating_cost
# Create a corrective operation against it
corrective_action = frappe.get_doc(
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
).insert()
corrective_job_card = make_corrective_job_card(
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
)
corrective_job_card.hour_rate = 100
corrective_job_card.insert()
corrective_job_card.append(
"time_logs",
{
"from_time": add_to_date(now(), hours=2),
"to_time": add_to_date(now(), hours=2, minutes=30),
"completed_qty": 2,
},
)
corrective_job_card.submit()
self.work_order.reload()
cost_after_correction = self.work_order.total_operating_cost
self.assertGreater(cost_after_correction, original_cost)
corrective_job_card.cancel()
self.work_order.reload()
cost_after_cancel = self.work_order.total_operating_cost
self.assertEqual(cost_after_cancel, original_cost)
def create_bom_with_multiple_operations():

View File

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

View File

@ -359,7 +359,7 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v14_0.update_employee_advance_status
erpnext.patches.v13_0.update_employee_advance_status
erpnext.patches.v13_0.add_cost_center_in_loans
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
@ -367,7 +367,8 @@ erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v13_0.requeue_recoverable_reposts
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note

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,
date_diff,
flt,
get_link_to_form,
getdate,
)
@ -45,6 +46,7 @@ class PayrollEntry(Document):
def before_submit(self):
self.validate_employee_details()
self.validate_payroll_payable_account()
if self.validate_attendance:
if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
@ -66,6 +68,14 @@ class PayrollEntry(Document):
if len(emp_with_sal_slip):
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
def validate_payroll_payable_account(self):
if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
frappe.throw(
_(
"Account type cannot be set for payroll payable account {0}, please remove and try again"
).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
)
def on_cancel(self):
frappe.delete_doc(
"Salary Slip",

View File

@ -234,7 +234,7 @@
},
{
"fieldname": "actual_start_date",
"fieldtype": "Data",
"fieldtype": "Date",
"label": "Actual Start Date (via Time Sheet)",
"read_only": 1
},
@ -458,7 +458,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2022-01-29 13:58:27.712714",
"modified": "2022-05-25 22:45:06.108499",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@ -504,4 +504,4 @@
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
}
}

View File

@ -90,7 +90,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
else {
return{
query: "erpnext.controllers.queries.item_query",
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1 }
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0}
}
}
});

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)) {
$.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) {
let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
let base_amount, amount;
if (me.frm.doc.party_account_currency == me.frm.doc.currency) {
// if customer/supplier currency is same as company currency
// total_amount_to_pay is already in customer/supplier currency
// so base_amount has to be calculated using total_amount_to_pay
base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data));
amount = flt(total_amount_to_pay, precision("amount", data));
} else {
base_amount = flt(total_amount_to_pay, precision("base_amount", data));
amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
}
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
frappe.model.set_value(data.doctype, data.name, "amount", amount);
payment_status = false;
} else if(me.frm.doc.paid_amount) {
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "field:hsn_code",
"creation": "2017-06-21 10:48:56.422086",
"doctype": "DocType",
@ -7,6 +8,7 @@
"field_order": [
"hsn_code",
"description",
"gst_rates",
"taxes"
],
"fields": [
@ -16,22 +18,37 @@
"in_list_view": 1,
"label": "HSN Code",
"reqd": 1,
"show_days": 1,
"show_seconds": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
"label": "Description",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "taxes",
"fieldtype": "Table",
"label": "Taxes",
"options": "Item Tax"
"options": "Item Tax",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "gst_rates",
"fieldtype": "Table",
"label": "GST Rates",
"options": "HSN Tax Rate",
"show_days": 1,
"show_seconds": 1
}
],
"modified": "2019-11-01 11:18:59.556931",
"links": [],
"modified": "2022-05-11 13:42:27.286643",
"modified_by": "Administrator",
"module": "Regional",
"name": "GST HSN Code",

View File

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

View File

@ -2,13 +2,14 @@
# For license information, please see license.txt
import json
import os
import frappe
from frappe import _
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.document import Document
from frappe.utils import date_diff, get_url, nowdate
from frappe.utils import date_diff, flt, get_url, nowdate
class EmailMissing(frappe.ValidationError):
@ -129,3 +130,31 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
)
return email_id
@frappe.whitelist()
def update_hsn_codes():
frappe.enqueue(enqueue_update)
frappe.msgprint(_("HSN/SAC Code sync started, this may take a few minutes..."))
def enqueue_update():
with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f:
hsn_codes = json.loads(f.read())
for hsn_code in hsn_codes:
try:
hsn_code_doc = frappe.get_doc("GST HSN Code", hsn_code.get("hsn_code"))
hsn_code_doc.set("gst_rates", [])
for rate in hsn_code.get("gst_rates"):
hsn_code_doc.append(
"gst_rates",
{
"minimum_taxable_value": flt(hsn_code.get("minimum_taxable_value")),
"tax_rate": flt(rate.get("tax_rate")),
},
)
hsn_code_doc.save()
except Exception as e:
pass

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

View File

@ -649,6 +649,8 @@ def make_einvoice(invoice):
try:
einvoice = safe_json_load(einvoice)
einvoice = santize_einvoice_fields(einvoice)
except json.JSONDecodeError:
raise
except Exception:
show_link_to_error_log(invoice, einvoice)
@ -765,7 +767,9 @@ def safe_json_load(json_string):
frappe.throw(
_(
"Error in input data. Please check for any special characters near following input: <br> {}"
).format(snippet)
).format(snippet),
title=_("Invalid JSON"),
exc=e,
)
@ -797,7 +801,8 @@ class GSPConnector:
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
@ -1185,6 +1190,7 @@ class GSPConnector:
headers = self.get_headers()
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"]
del headers["user_name"]
try:
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
if res.get("success"):
@ -1358,9 +1364,13 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
def cancel_eway_bill(doctype, docname):
# NOTE: cancel_eway_bill api is disabled by Adequare.
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
frappe.db.set_value(doctype, docname, "ewaybill", "")
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
@frappe.whitelist()

View File

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

View File

@ -840,6 +840,30 @@ def get_gst_accounts(
return gst_accounts
def validate_sez_and_export_invoices(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")
if country != "India":
return
if (
doc.get("gst_category") in ("SEZ", "Overseas")
and doc.get("export_type") == "Without Payment of Tax"
):
gst_accounts = get_gst_accounts(doc.company)
for tax in doc.get("taxes"):
for tax in doc.get("taxes"):
if (
tax.account_head
in gst_accounts.get("igst_account", [])
+ gst_accounts.get("sgst_account", [])
+ gst_accounts.get("cgst_account", [])
and tax.tax_amount_after_discount_amount
):
frappe.throw(_("GST cannot be applied on SEZ or Export invoices without payment of tax"))
def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")
@ -887,6 +911,8 @@ def validate_reverse_charge_transaction(doc, method):
frappe.throw(msg)
doc.eligibility_for_itc = "ITC on Reverse Charge"
def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")

View File

@ -226,7 +226,10 @@ class Gstr1Report(object):
taxable_value += abs(net_amount)
elif (
not tax_rate
and self.filters.get("type_of_business") == "EXPORT"
and (
self.filters.get("type_of_business") == "EXPORT"
or invoice_details.get("gst_category") == "SEZ"
)
and invoice_details.get("export_type") == "Without Payment of Tax"
):
taxable_value += abs(net_amount)
@ -328,12 +331,14 @@ class Gstr1Report(object):
def get_invoice_items(self):
self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict()
self.item_hsn_map = frappe._dict()
self.nil_exempt_non_gst = {}
# nosemgrep
items = frappe.db.sql(
"""
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
is_non_gst from `tab%s Item`
gst_hsn_code, is_non_gst from `tab%s Item`
where parent in (%s)
"""
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
@ -343,6 +348,7 @@ class Gstr1Report(object):
for d in items:
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
self.item_hsn_map.setdefault(d.item_code, d.gst_hsn_code)
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
"base_net_amount", 0
)
@ -367,6 +373,8 @@ class Gstr1Report(object):
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
def get_items_based_on_tax_rate(self):
hsn_wise_tax_rate = get_hsn_wise_tax_rates()
self.tax_details = frappe.db.sql(
"""
select
@ -427,7 +435,7 @@ class Gstr1Report(object):
alert=True,
)
# Build itemised tax for export invoices where tax table is blank
# Build itemised tax for export invoices where tax table is blank (Export and SEZ Invoices)
for invoice, items in self.invoice_items.items():
if (
invoice not in self.items_based_on_tax_rate
@ -435,7 +443,17 @@ class Gstr1Report(object):
and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax"
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ")
):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
self.items_based_on_tax_rate.setdefault(invoice, {})
for item_code in items.keys():
hsn_code = self.item_hsn_map.get(item_code)
tax_rate = 0
taxable_value = items.get(item_code)
for rates in hsn_wise_tax_rate.get(hsn_code, []):
if taxable_value > rates.get("minimum_taxable_value"):
tax_rate = rates.get("tax_rate")
self.items_based_on_tax_rate[invoice].setdefault(tax_rate, [])
self.items_based_on_tax_rate[invoice][tax_rate].append(item_code)
def get_columns(self):
self.other_columns = []
@ -728,7 +746,7 @@ def get_json(filters, report_name, data):
elif filters["type_of_business"] == "EXPORT":
for item in report_data[:-1]:
res.setdefault(item["export_type"], []).append(item)
res.setdefault(item["export_type"], {}).setdefault(item["invoice_number"], []).append(item)
out = get_export_json(res)
gst_json["exp"] = out
@ -918,11 +936,21 @@ def get_export_json(res):
for exp_type in res:
exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
for row in res[exp_type]:
inv_item = get_basic_invoice_detail(row)
inv_item["itms"] = [
{"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0}
]
for number, invoice in res[exp_type].items():
inv_item = get_basic_invoice_detail(invoice[0])
inv_item["itms"] = []
for item in invoice:
inv_item["itms"].append(
{
"txval": flt(item["taxable_value"], 2),
"rt": flt(item["rate"]),
"iamt": flt((item["taxable_value"] * flt(item["rate"])) / 100.0, 2)
if exp_type != "WOPAY"
else 0,
"csamt": (flt(item.get("cess_amount"), 2) or 0),
}
)
inv.append(inv_item)
@ -1060,7 +1088,6 @@ def get_rate_and_tax_details(row, gstin):
# calculate tax amount added
tax = flt((row["taxable_value"] * rate) / 100.0, 2)
frappe.errprint([tax, tax / 2])
if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
else:
@ -1136,3 +1163,26 @@ def get_company_gstins(company):
address_list = [""] + [d.gstin for d in addresses]
return address_list
def get_hsn_wise_tax_rates():
hsn_wise_tax_rate = {}
gst_hsn_code = frappe.qb.DocType("GST HSN Code")
hsn_tax_rates = frappe.qb.DocType("HSN Tax Rate")
hsn_code_data = (
frappe.qb.from_(gst_hsn_code)
.inner_join(hsn_tax_rates)
.on(gst_hsn_code.name == hsn_tax_rates.parent)
.select(gst_hsn_code.hsn_code, hsn_tax_rates.tax_rate, hsn_tax_rates.minimum_taxable_value)
.orderby(hsn_tax_rates.minimum_taxable_value)
.run(as_dict=1)
)
for d in hsn_code_data:
hsn_wise_tax_rate.setdefault(d.hsn_code, [])
hsn_wise_tax_rate[d.hsn_code].append(
{"minimum_taxable_value": d.minimum_taxable_value, "tax_rate": d.tax_rate}
)
return hsn_wise_tax_rate

View File

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

View File

@ -367,7 +367,14 @@ def set_credit_limit(customer, company, credit_limit):
customer.credit_limits[-1].db_insert()
def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
def create_internal_customer(
customer_name=None, represents_company=None, allowed_to_interact_with=None
):
if not customer_name:
customer_name = represents_company
if not allowed_to_interact_with:
allowed_to_interact_with = represents_company
if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc(
{

View File

@ -232,7 +232,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used")
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO

View File

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

View File

@ -64,7 +64,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer, 'has_variants': 0}
}
});
}

View File

@ -54,5 +54,35 @@ frappe.ui.form.on("Naming Series", {
frm.events.get_doc_and_prefix(frm);
}
});
}
},
naming_series_to_check(frm) {
frappe.call({
method: "preview_series",
doc: frm.doc,
callback: function(r) {
if (!r.exc) {
frm.set_value("preview", r.message);
} else {
frm.set_value("preview", __("Failed to generate preview of series"));
}
}
});
},
add_series(frm) {
const series = frm.doc.naming_series_to_check;
if (!series) {
frappe.show_alert(__("Please type a valid series."));
return;
}
if (!frm.doc.set_options.includes(series)) {
const current_series = frm.doc.set_options;
frm.set_value("set_options", `${current_series}\n${series}`);
} else {
frappe.show_alert(__("Series already added to transaction."));
}
},
});

View File

@ -1,360 +1,132 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-01-25 11:35:08",
"custom": 0,
"description": "Set prefix for numbering series on your transactions",
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"actions": [],
"creation": "2022-05-26 03:12:49.087648",
"description": "Set prefix for numbering series on your transactions",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"setup_series",
"select_doc_for_series",
"help_html",
"naming_series_to_check",
"preview",
"add_series",
"set_options",
"user_must_always_select",
"update",
"column_break_13",
"update_series",
"prefix",
"current_value",
"update_series_start"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Set prefix for numbering series on your transactions",
"fieldname": "setup_series",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Setup Series",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Set prefix for numbering series on your transactions",
"fieldname": "setup_series",
"fieldtype": "Section Break",
"label": "Setup Series"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "select_doc_for_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Select Transaction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "select_doc_for_series",
"fieldtype": "Select",
"label": "Select Transaction"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "select_doc_for_series",
"fieldname": "help_html",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Help HTML",
"length": 0,
"no_copy": 0,
"options": "<div class=\"well\">\nEdit list of Series in the box below. Rules:\n<ul>\n<li>Each Series Prefix on a new line.</li>\n<li>Allowed special characters are \"/\" and \"-\"</li>\n<li>Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.</li>\n</ul>\nExamples:<br>\nINV-<br>\nINV-10-<br>\nINVK-<br>\nINV-.####<br>\n</div>",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "select_doc_for_series",
"fieldname": "help_html",
"fieldtype": "HTML",
"label": "Help HTML",
"options": "<div class=\"well\">\n Edit list of Series in the box below. Rules:\n <ul>\n <li>Each Series Prefix on a new line.</li>\n <li>Allowed special characters are \"/\" and \"-\"</li>\n <li>\n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n </li>\n <li>\n You can also use variables in the series name by putting them\n between (.) dots\n <br>\n Support Variables:\n <ul>\n <li><code>.YYYY.</code> - Year in 4 digits</li>\n <li><code>.YY.</code> - Year in 2 digits</li>\n <li><code>.MM.</code> - Month</li>\n <li><code>.DD.</code> - Day of month</li>\n <li><code>.WW.</code> - Week of the year</li>\n <li><code>.FY.</code> - Fiscal Year</li>\n <li>\n <code>.{fieldname}.</code> - fieldname on the document e.g.\n <code>branch</code>\n </li>\n </ul>\n </li>\n </ul>\n Examples:\n <ul>\n <li>INV-</li>\n <li>INV-10-</li>\n <li>INVK-</li>\n <li>INV-.YYYY.-.{branch}.-.MM.-.####</li>\n </ul>\n</div>\n<br>\n"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "select_doc_for_series",
"fieldname": "set_options",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series List for this Transaction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "select_doc_for_series",
"fieldname": "set_options",
"fieldtype": "Text",
"label": "Series List for this Transaction"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "select_doc_for_series",
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
"fieldname": "user_must_always_select",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "User must always select",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "0",
"depends_on": "select_doc_for_series",
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
"fieldname": "user_must_always_select",
"fieldtype": "Check",
"label": "User must always select"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "select_doc_for_series",
"fieldname": "update",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Update",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "select_doc_for_series",
"fieldname": "update",
"fieldtype": "Button",
"label": "Update"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Change the starting / current sequence number of an existing series.",
"fieldname": "update_series",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Update Series",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Change the starting / current sequence number of an existing series.",
"fieldname": "update_series",
"fieldtype": "Section Break",
"label": "Update Series"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prefix",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Prefix",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "prefix",
"fieldtype": "Select",
"label": "Prefix"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "This is the number of the last created transaction with this prefix",
"fieldname": "current_value",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "This is the number of the last created transaction with this prefix",
"fieldname": "current_value",
"fieldtype": "Int",
"label": "Current Value"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "update_series_start",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Update Series Number",
"length": 0,
"no_copy": 0,
"options": "update_series_start",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "update_series_start",
"fieldtype": "Button",
"label": "Update Series Number",
"options": "update_series_start"
},
{
"fieldname": "naming_series_to_check",
"fieldtype": "Data",
"label": "Try a naming Series"
},
{
"default": " ",
"fieldname": "preview",
"fieldtype": "Text",
"label": "Preview of generated names",
"read_only": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "add_series",
"fieldtype": "Button",
"label": "Add this Series"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "fa fa-sort-by-order",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-08-17 03:41:37.685910",
"modified_by": "Administrator",
"module": "Setup",
"name": "Naming Series",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "fa fa-sort-by-order",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2022-05-26 06:06:42.109504",
"modified_by": "Administrator",
"module": "Setup",
"name": "Naming Series",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _, msgprint, throw
from frappe.core.doctype.doctype.doctype import validate_series
from frappe.model.document import Document
from frappe.model.naming import parse_naming_series
from frappe.model.naming import make_autoname, parse_naming_series
from frappe.permissions import get_doctypes_with_read
from frappe.utils import cint, cstr
@ -206,6 +206,35 @@ class NamingSeries(Document):
prefix = parse_naming_series(parts)
return prefix
@frappe.whitelist()
def preview_series(self) -> str:
"""Preview what the naming series will generate."""
generated_names = []
series = self.naming_series_to_check
if not series:
return ""
try:
doc = self._fetch_last_doc_if_available()
for _count in range(3):
generated_names.append(make_autoname(series, doc=doc))
except Exception as e:
if frappe.message_log:
frappe.message_log.pop()
return _("Failed to generate names from the series") + f"\n{str(e)}"
# Explcitly rollback in case any changes were made to series table.
frappe.db.rollback() # nosemgrep
return "\n".join(generated_names)
def _fetch_last_doc_if_available(self):
"""Fetch last doc for evaluating naming series with fields."""
try:
return frappe.get_last_doc(self.select_doc_for_series)
except Exception:
return None
def set_by_naming_series(
doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1

View File

@ -0,0 +1,35 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.setup.doctype.naming_series.naming_series import NamingSeries
class TestNamingSeries(FrappeTestCase):
def setUp(self):
self.ns: NamingSeries = frappe.get_doc("Naming Series")
def tearDown(self):
frappe.db.rollback()
def test_naming_preview(self):
self.ns.select_doc_for_series = "Sales Invoice"
self.ns.naming_series_to_check = "AXBZ.####"
serieses = self.ns.preview_series().split("\n")
self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses)
self.ns.naming_series_to_check = "AXBZ-.{currency}.-"
serieses = self.ns.preview_series().split("\n")
def test_get_transactions(self):
naming_info = self.ns.get_transactions()
self.assertIn("Sales Invoice", naming_info["transactions"])
existing_naming_series = frappe.get_meta("Sales Invoice").get_field("naming_series").options
for series in existing_naming_series.split("\n"):
self.assertIn(series, naming_info["prefixes"])

View File

@ -86,20 +86,29 @@ def get_batch_naming_series():
class Batch(Document):
def autoname(self):
"""Generate random ID for batch if not specified"""
if not self.batch_id:
create_new_batch, batch_number_series = frappe.db.get_value(
"Item", self.item, ["create_new_batch", "batch_number_series"]
)
if create_new_batch:
if batch_number_series:
self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series()
else:
self.batch_id = get_name_from_hash()
if self.batch_id:
self.name = self.batch_id
return
create_new_batch, batch_number_series = frappe.db.get_value(
"Item", self.item, ["create_new_batch", "batch_number_series"]
)
if not create_new_batch:
frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError)
while not self.batch_id:
if batch_number_series:
self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series()
else:
frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError)
self.batch_id = get_name_from_hash()
# User might have manually created a batch with next number
if frappe.db.exists("Batch", self.batch_id):
self.batch_id = None
self.name = self.batch_id

View File

@ -11,6 +11,8 @@ from frappe.utils.data import add_to_date, getdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
@ -27,7 +29,7 @@ class TestBatch(FrappeTestCase):
)
@classmethod
def make_batch_item(cls, item_name):
def make_batch_item(cls, item_name=None):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
@ -245,7 +247,7 @@ class TestBatch(FrappeTestCase):
if not use_naming_series:
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0)
def make_new_batch(self, item_name, batch_id=None, do_not_insert=0):
def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0):
batch = frappe.new_doc("Batch")
item = self.make_batch_item(item_name)
batch.item = item.name
@ -407,6 +409,26 @@ class TestBatch(FrappeTestCase):
self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date))
def test_autocreation_of_batches(self):
"""
Test if auto created Serial No excludes existing serial numbers
"""
item_code = make_item(
properties={
"has_batch_no": 1,
"batch_number_series": "BATCHEXISTING.###",
"create_new_batch": 1,
}
).name
manually_created_batch = self.make_new_batch(item_code, batch_id="BATCHEXISTING001").name
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch)
pr_2 = make_purchase_receipt(item_code=item_code, qty=1)
self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no)
self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no)
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(

View File

@ -570,15 +570,12 @@ class TestDeliveryNote(FrappeTestCase):
customer=customer_name,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
do_not_submit=True,
qty=5,
rate=500,
warehouse="Stores - TCP1",
target_warehouse=target_warehouse,
)
dn.submit()
# qty after delivery
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
self.assertEqual(actual_qty_at_source, 475)
@ -1000,6 +997,73 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn2.items[0].returned_qty, 0)
self.assertEqual(dn2.per_billed, 100)
def test_internal_transfer_with_valuation_only(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
item = make_item().name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 42
# Create item price and pricing rule
frappe.get_doc(
{
"item_code": item,
"price_list": "Standard Selling",
"price_list_rate": 1000,
"doctype": "Item Price",
}
).insert()
frappe.get_doc(
{
"doctype": "Pricing Rule",
"title": frappe.generate_hash(),
"apply_on": "Item Code",
"price_or_product_discount": "Price",
"selling": 1,
"company": company,
"margin_type": "Percentage",
"margin_rate_or_amount": 10,
"apply_discount_on": "Grand Total",
"items": [
{
"item_code": item,
}
],
}
).insert()
make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
dn = create_delivery_note(
item_code=item,
company=company,
customer=customer,
qty=5,
rate=500,
warehouse=warehouse,
target_warehouse=target,
ignore_pricing_rule=0,
do_not_save=True,
do_not_submit=True,
)
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
dn.save()
self.assertEqual(dn.ignore_pricing_rule, 1)
# rate should reset to incoming rate
self.assertEqual(dn.items[0].rate, rate)
# rate should reset again if discounts are fiddled with
dn.items[0].margin_type = "Amount"
dn.items[0].margin_rate_or_amount = 50
dn.save()
self.assertEqual(dn.items[0].rate, rate)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
@ -12,6 +10,7 @@ from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import (
create_item,
make_item,
make_item_variant,
set_item_variant_settings,
)
@ -1443,6 +1442,21 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(mapped_se.items[0].basic_rate, 100)
self.assertEqual(mapped_se.items[0].basic_amount, 200)
def test_stock_entry_item_details(self):
item = make_item()
se = make_stock_entry(
item_code=item.name, qty=1, to_warehouse="_Test Warehouse - _TC", do_not_submit=True
)
self.assertEqual(se.items[0].item_name, item.item_name)
se.items[0].item_name = "wat"
se.items[0].stock_uom = "Kg"
se.save()
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@ -8,9 +8,8 @@ import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.query_builder.functions import CombineDatetime
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from frappe.utils.data import add_to_date
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_to_date, flt, today
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@ -1219,6 +1218,41 @@ class TestStockLedgerEntry(FrappeTestCase):
except Exception as e:
self.fail("Double processing of qty for clashing timestamp.")
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
def test_transfer_invariants(self):
"""Extact stock value should be transferred."""
item = make_item(
properties={
"valuation_method": "Moving Average",
"stock_uom": "Kg",
}
).name
source_warehouse = "Stores - TCP1"
target_warehouse = "Finished Goods - TCP1"
make_purchase_receipt(
item=item,
warehouse=source_warehouse,
qty=20,
conversion_factor=1000,
uom="Tonne",
rate=156_526.0,
company="_Test Company with perpetual inventory",
)
transfer = make_stock_entry(
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
)
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0}
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["*"],
filters=filters,
order_by="timestamp(posting_date, posting_time), creation",
)
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
def create_repack_entry(**args):
args = frappe._dict(args)

View File

@ -1,88 +1,97 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Warehouse", {
onload: function(frm) {
frm.set_query("default_in_transit_warehouse", function() {
setup: function (frm) {
frm.set_query("default_in_transit_warehouse", function (doc) {
return {
filters:{
'warehouse_type' : 'Transit',
'is_group': 0,
'company': frm.doc.company
}
filters: {
warehouse_type: "Transit",
is_group: 0,
company: doc.company,
},
};
});
frm.set_query("parent_warehouse", function () {
return {
filters: {
is_group: 1,
},
};
});
frm.set_query("account", function (doc) {
return {
filters: {
is_group: 0,
account_type: "Stock",
company: doc.company,
},
};
});
},
refresh: function(frm) {
frm.toggle_display('warehouse_name', frm.doc.__islocal);
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
refresh: function (frm) {
frm.toggle_display("warehouse_name", frm.doc.__islocal);
frm.toggle_display(
["address_html", "contact_html"],
!frm.doc.__islocal
);
if(!frm.doc.__islocal) {
if (!frm.doc.__islocal) {
frappe.contacts.render_address_and_contact(frm);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
frm.add_custom_button(__("Stock Balance"), function() {
frappe.set_route("query-report", "Stock Balance", {"warehouse": frm.doc.name});
frm.add_custom_button(__("Stock Balance"), function () {
frappe.set_route("query-report", "Stock Balance", {
warehouse: frm.doc.name,
});
});
if (cint(frm.doc.is_group) == 1) {
frm.add_custom_button(__('Group to Non-Group'),
function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default')
} else if (cint(frm.doc.is_group) == 0) {
if(frm.doc.__onload && frm.doc.__onload.account) {
frm.add_custom_button(__("General Ledger"), function() {
frm.add_custom_button(
frm.doc.is_group
? __("Convert to Ledger", null, "Warehouse")
: __("Convert to Group", null, "Warehouse"),
function () {
convert_to_group_or_ledger(frm);
},
);
if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) {
frm.add_custom_button(
__("General Ledger", null, "Warehouse"),
function () {
frappe.route_options = {
"account": frm.doc.__onload.account,
"company": frm.doc.company
}
account: frm.doc.__onload.account,
company: frm.doc.company,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.add_custom_button(__('Non-Group to Group'),
function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default')
}
frm.toggle_enable(['is_group', 'company'], false);
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Warehouse'};
frm.fields_dict['parent_warehouse'].get_query = function(doc) {
return {
filters: {
"is_group": 1,
}
}
);
}
frm.fields_dict['account'].get_query = function(doc) {
return {
filters: {
"is_group": 0,
"account_type": "Stock",
"company": frm.doc.company
}
}
}
}
frm.toggle_enable(["is_group", "company"], false);
frappe.dynamic_link = {
doc: frm.doc,
fieldname: "name",
doctype: "Warehouse",
};
},
});
function convert_to_group_or_ledger(frm){
function convert_to_group_or_ledger(frm) {
frappe.call({
method:"erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger",
method: "erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger",
args: {
docname: frm.doc.name,
is_group: frm.doc.is_group
is_group: frm.doc.is_group,
},
callback: function(){
callback: function () {
frm.refresh();
}
})
},
});
}

View File

@ -199,7 +199,7 @@ def process_args(args):
if not args.get("price_list"):
args.price_list = args.get("selling_price_list") or args.get("buying_price_list")
if args.barcode:
if not args.item_code and args.barcode:
args.item_code = get_item_code(barcode=args.barcode)
elif not args.item_code and args.serial_no:
args.item_code = get_item_code(serial_no=args.serial_no)

View File

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

View File

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

View File

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

View File

@ -783,7 +783,7 @@ Default Activity Cost exists for Activity Type - {0},Es gibt Standard-Aktivität
Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein,
Default BOM for {0} not found,Standardstückliste für {0} nicht gefunden,
Default BOM not found for Item {0} and Project {1},Standard-Stückliste nicht gefunden für Position {0} und Projekt {1},
Default In-Transit Warehouse, Standardlager für Waren im Transit,
Default In-Transit Warehouse,Standard-Durchgangslager,
Default Letter Head,Standardbriefkopf,
Default Tax Template,Standardsteuervorlage,
Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,"Die Standard-Maßeinheit für Artikel {0} kann nicht direkt geändert werden, weil Sie bereits einige Transaktionen mit einer anderen Maßeinheit durchgeführt haben. Sie müssen einen neuen Artikel erstellen, um eine andere Standard-Maßeinheit verwenden zukönnen.",
@ -1178,7 +1178,7 @@ Group by Party,Gruppieren nach Partei,
Group by Voucher,Gruppieren nach Beleg,
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt,
Group to Non-Group,Gruppe an konzernfremde,
Convert to Ledger,In Lagerbuch umwandeln,Warehouse
Group your students in batches,Gruppieren Sie Ihre Schüler in den Reihen,
Groups,Gruppen,
Guardian1 Email ID,Guardian1 E-Mail-ID,
@ -1701,7 +1701,7 @@ No Permission,Keine Berechtigung,
No Remarks,Keine Anmerkungen,
No Result to submit,Kein Ergebnis zur Einreichung,
No Salary Structure assigned for Employee {0} on given date {1},Keine Gehaltsstruktur für Mitarbeiter {0} am angegebenen Datum {1} zugewiesen,
No Staffing Plans found for this Designation,Für diese Bezeichnung wurden keine Stellenpläne gefunden,
No Staffing Plans found for this Designation,Für diese Position wurden keine Stellenpläne gefunden,
No Student Groups created.,Keine Studentengruppen erstellt.,
No Students in,Keine Studenten in,
No Tax Withholding data found for the current Fiscal Year.,Keine Steuerverweigerungsdaten für das aktuelle Geschäftsjahr gefunden.,
@ -1735,7 +1735,6 @@ Non GST Inward Supplies,Nicht GST Inward Supplies,
Non Profit,Gemeinnützig,
Non Profit (beta),Non-Profit (Beta),
Non-GST outward supplies,Nicht-GST-Lieferungen nach außen,
Non-Group to Group,Non-Group-Gruppe,
None,Keiner,
None of the items have any change in quantity or value.,Keiner der Artikel hat irgendeine Änderung bei Mengen oder Kosten.,
Nos,Stk,
@ -2027,7 +2026,7 @@ Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine
Please select Category first,Bitte zuerst Kategorie auswählen,
Please select Charge Type first,Bitte zuerst Chargentyp auswählen,
Please select Company,Bitte Unternehmen auswählen,
Please select Company and Designation,Bitte wählen Sie Unternehmen und Stelle,
Please select Company and Designation,Bitte wählen Sie Unternehmen und Position,
Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten",
Please select Company first,Bitte zuerst Unternehmen auswählen,
Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert,
@ -2772,7 +2771,7 @@ Split,Teilt,
Split Batch,Split Batch,
Split Issue,Split-Problem,
Sports,Sport,
Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Bezeichnung {1},
Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Position {1},
Standard,Standard,
Standard Buying,Standard-Kauf,
Standard Selling,Standard-Vertrieb,
@ -3710,7 +3709,7 @@ Delivered Quantity,Gelieferte Menge,
Delivery Notes,Lieferscheine,
Depreciated Amount,Abschreibungsbetrag,
Description,Beschreibung,
Designation,Bezeichnung,
Designation,Position,
Difference Value,Differenzwert,
Dimension Filter,Dimensionsfilter,
Disabled,Deaktiviert,
@ -3920,7 +3919,7 @@ Please enter <b>Difference Account</b> or set default <b>Stock Adjustment Accoun
Please enter GSTIN and state for the Company Address {0},Bitte geben Sie GSTIN ein und geben Sie die Firmenadresse {0} an.,
Please enter Item Code to get item taxes,"Bitte geben Sie den Artikelcode ein, um die Artikelsteuern zu erhalten",
Please enter Warehouse and Date,Bitte geben Sie Lager und Datum ein,
Please enter the designation,Bitte geben Sie die Bezeichnung ein,
Please enter the designation,Bitte geben Sie die Position ein,
Please login as a Marketplace User to edit this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu bearbeiten.",
Please login as a Marketplace User to report this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu melden.",
Please select <b>Template Type</b> to download template,"Bitte wählen Sie <b>Vorlagentyp</b> , um die Vorlage herunterzuladen",
@ -6243,7 +6242,7 @@ Checking this will create Lab Test(s) specified in the Sales Invoice on submissi
Create Sample Collection document for Lab Test,Erstellen Sie ein Probensammeldokument für den Labortest,
Checking this will create a Sample Collection document every time you create a Lab Test,"Wenn Sie dies aktivieren, wird jedes Mal, wenn Sie einen Labortest erstellen, ein Probensammeldokument erstellt",
Employee name and designation in print,Name und Bezeichnung des Mitarbeiters im Druck,
Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Bezeichnung des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.",
Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Position des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.",
Do not print or email Lab Tests without Approval,Drucken oder senden Sie Labortests nicht ohne Genehmigung per E-Mail,
Checking this will restrict printing and emailing of Lab Test documents unless they have the status as Approved.,"Wenn Sie dies aktivieren, wird das Drucken und E-Mailen von Labortestdokumenten eingeschränkt, sofern diese nicht den Status &quot;Genehmigt&quot; haben.",
Custom Signature in Print,Kundenspezifische Unterschrift im Druck,
@ -6499,7 +6498,7 @@ Department Approver,Abteilungsgenehmiger,
Approver,Genehmiger,
Required Skills,Benötigte Fähigkeiten,
Skills,Kompetenzen,
Designation Skill,Bezeichnung Fähigkeit,
Designation Skill,Positions Fähigkeit,
Skill,Fertigkeit,
Driver,Fahrer/-in,
HR-DRI-.YYYY.-,HR-DRI-.YYYY.-,
@ -6798,7 +6797,7 @@ Select Employees,Mitarbeiter auswählen,
Employment Type (optional),Anstellungsart (optional),
Branch (optional),Zweigstelle (optional),
Department (optional),Abteilung (optional),
Designation (optional),Bezeichnung (optional),
Designation (optional),Position (optional),
Employee Grade (optional),Dienstgrad (optional),
Employee (optional),Mitarbeiter (optional),
Allocate Leaves,Blätter zuweisen,
@ -7653,7 +7652,7 @@ Campaign Schedules,Kampagnenpläne,
Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen.,
CUST-.YYYY.-,CUST-.YYYY.-,
Default Company Bank Account,Standard-Bankkonto des Unternehmens,
From Lead,Von Lead,
From Lead,Aus Lead,
Account Manager,Buchhalter,
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
@ -7769,7 +7768,7 @@ Authorized Value,Autorisierter Wert,
Applicable To (Role),Anwenden auf (Rolle),
Applicable To (Employee),Anwenden auf (Mitarbeiter),
Applicable To (User),Anwenden auf (Benutzer),
Applicable To (Designation),Anwenden auf (Bezeichnung),
Applicable To (Designation),Anwenden auf (Position),
Approving Role (above authorized value),Genehmigende Rolle (über dem autorisierten Wert),
Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert),
Brand Defaults,Markenstandards,
@ -8946,7 +8945,7 @@ Requesting Practitioner,Praktizierender anfordern,
Requesting Department,Abteilung anfordern,
Employee (Lab Technician),Mitarbeiter (Labortechniker),
Lab Technician Name,Name des Labortechnikers,
Lab Technician Designation,Bezeichnung des Labortechnikers,
Lab Technician Designation,Position des Labortechnikers,
Compound Test Result,Zusammengesetztes Testergebnis,
Organism Test Result,Organismustestergebnis,
Sensitivity Test Result,Empfindlichkeitstestergebnis,
@ -9852,3 +9851,24 @@ Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen
{} Available,{} Verfügbar,
Report an Issue,Ein Problem melden,
User Forum,Anwenderforum,
Get Customer Group Details,Einstellungen aus Kundengruppe übernehmen,
Is Rate Adjustment Entry (Debit Note),Ist Preisanpassung (Belastungsanzeige),
Fetch Timesheet,Zeiterfassung laden,
Company Tax ID,Eigene Steuernummer,
Quotation Number,Angebotsnummer,
Company Shipping Address,Eigene Lieferadresse,
Company Billing Address,Eigene Rechnungsadresse,
Billing Address Details,Vorschau Rechnungsadresse,
Supplier Contact,Lieferantenkontakt,
Order Status,Bestellstatus,
Invoice Portion (%),Rechnungsanteil (%),
Discount Settings,Rabatt-Einstellungen,
Payment Amount (Company Currency),Zahlungsbetrag (Unternehmenswährung),
Putaway Rule,Einlagerungsregel,
Apply Putaway Rule,Einlagerungsregel anwenden,
Default Discount Account,Standard-Rabattkonto,
Default Provisional Account,Standard Provisorisches Konto,
Leave Type Allocation,Zuordnung Abwesenheitsarten,
From Lead,Aus Lead,
From Opportunity,Aus Chance,
Publish in Website,Auf Webseite veröffentlichen,

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

File diff suppressed because it is too large Load Diff