mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 03:19:49 +00:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da6237d22 | ||
|
|
526e350d98 | ||
|
|
e53a96ae1d | ||
|
|
3aab6e6fa8 | ||
|
|
2e9e6eef05 | ||
|
|
4bdea436e3 | ||
|
|
dbe289e734 | ||
|
|
8c4f45307e | ||
|
|
7b2dc2449d | ||
|
|
78bd698f9e | ||
|
|
544e37ca5c | ||
|
|
c7bdb1bbf9 | ||
|
|
5f5fa843ac | ||
|
|
c8bde399e5 | ||
|
|
7f83d15bda | ||
|
|
ad5eb6da4e | ||
|
|
3574d490db | ||
|
|
f17b2de420 | ||
|
|
d5efeec0a4 | ||
|
|
8ddbac5158 | ||
|
|
6e492ec514 | ||
|
|
00518eb384 | ||
|
|
661030aba1 | ||
|
|
19dda807d1 | ||
|
|
0d5abf1c95 | ||
|
|
ae88ba5d18 | ||
|
|
d855b532e4 | ||
|
|
4e38e8da1b | ||
|
|
6eeac48f17 | ||
|
|
d2a1acc2e2 | ||
|
|
a3aa4d536a | ||
|
|
cfcbdfcaec | ||
|
|
499987040b | ||
|
|
28027a9f94 | ||
|
|
b4aabf3f35 | ||
|
|
cf49b0effb | ||
|
|
0bb43a1be5 | ||
|
|
f4d07cc84e | ||
|
|
809d6d638e | ||
|
|
0834cb1bb6 | ||
|
|
2f62a9641e | ||
|
|
cdf73bb781 | ||
|
|
238769e6b5 | ||
|
|
91cad9e985 | ||
|
|
7b9784ce10 | ||
|
|
fc10c8e44e | ||
|
|
88c5de533a | ||
|
|
a24f0507e1 | ||
|
|
470dc10b15 | ||
|
|
4d25091196 | ||
|
|
44df522655 | ||
|
|
8694d22b7a | ||
|
|
072c7e913d | ||
|
|
bc12269ef4 | ||
|
|
7717a8a5e3 | ||
|
|
fd5d2ed87f | ||
|
|
c01bed9862 | ||
|
|
52108d52e2 | ||
|
|
e74e02b765 | ||
|
|
b70a37f6fa | ||
|
|
5157f5dd0e | ||
|
|
9ce5d84951 | ||
|
|
b712aea3a4 | ||
|
|
42bda6e37b | ||
|
|
eb24f91341 | ||
|
|
341eab2b2a | ||
|
|
0857632359 | ||
|
|
e23f1555bb | ||
|
|
75c844a15a | ||
|
|
d1171016b3 | ||
|
|
563f83f0f5 | ||
|
|
9844508066 | ||
|
|
4824302811 | ||
|
|
08b9aaff26 | ||
|
|
eb1f8f932d | ||
|
|
07ff956fd8 | ||
|
|
c575942acf | ||
|
|
6841e22ffe | ||
|
|
56a422deed | ||
|
|
99b201d5d7 | ||
|
|
5bc2b8f685 | ||
|
|
7a159a7187 | ||
|
|
0ec34e5880 | ||
|
|
ba58c7ed59 | ||
|
|
3a1475a90b | ||
|
|
65c0189c4d | ||
|
|
1b2c4bf868 | ||
|
|
88ed6e6cb4 | ||
|
|
8c5322c1cb | ||
|
|
82a8f2b1b2 | ||
|
|
3908b510bd | ||
|
|
14547d94b3 | ||
|
|
9bea2fcdfc | ||
|
|
bb55210f49 | ||
|
|
178be42369 | ||
|
|
b4e775b264 | ||
|
|
e6945508f1 | ||
|
|
5354169f31 | ||
|
|
ba66a6714c | ||
|
|
573cd3c33b | ||
|
|
b6edadb3cb | ||
|
|
d65df443fc | ||
|
|
f6607a6050 | ||
|
|
75d98ef205 | ||
|
|
fd1d2cd203 | ||
|
|
c66dc5658f | ||
|
|
1ebf2dd2bf | ||
|
|
de631e65cc | ||
|
|
02f2844db2 | ||
|
|
c4d9576f9f | ||
|
|
74303b65cf | ||
|
|
a34aff6f49 | ||
|
|
f105c1bd5e | ||
|
|
264c314416 | ||
|
|
a71a336e59 | ||
|
|
7db3645298 | ||
|
|
b1ecca3a16 | ||
|
|
cbfa188d3d | ||
|
|
4f7344c278 | ||
|
|
45645c1064 | ||
|
|
d44da6c820 | ||
|
|
95ea28f14d | ||
|
|
7971c149ed | ||
|
|
3d7b2b1a6d | ||
|
|
9942a9d40a | ||
|
|
9a607b9bd0 | ||
|
|
304e6bb996 | ||
|
|
4a557b47d7 | ||
|
|
71767994a7 | ||
|
|
2039bd066d | ||
|
|
b599b93ae8 | ||
|
|
59d579764d | ||
|
|
5f25cea322 | ||
|
|
6a0c24e7b3 | ||
|
|
8eb6053c97 | ||
|
|
c8ec365594 | ||
|
|
0fbd29b16d | ||
|
|
806f7e5eef | ||
|
|
59c6eb591b | ||
|
|
0490e3bfe6 | ||
|
|
0aeef34944 | ||
|
|
69f1247fab | ||
|
|
2d01b72b04 | ||
|
|
bb4c968d95 | ||
|
|
c40aa580c5 | ||
|
|
642692a040 | ||
|
|
fd24d52d86 | ||
|
|
91a95adcb6 | ||
|
|
fe04b5a2b9 | ||
|
|
d1b611d37f | ||
|
|
e98b34617f | ||
|
|
6391ccd56a | ||
|
|
b848b77815 | ||
|
|
d82ba4e86f | ||
|
|
aea9d82672 | ||
|
|
a3a9cd5174 |
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 60
|
||||
|
||||
name: Patch Test
|
||||
|
||||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
|
||||
2
.github/workflows/translation_linter.yml
vendored
2
.github/workflows/translation_linter.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.47.0"
|
||||
__version__ = "13.49.9"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -7,7 +7,7 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils import cint, comma_or, flt, get_link_to_form, getdate, nowdate
|
||||
from six import iteritems, string_types
|
||||
|
||||
import erpnext
|
||||
@@ -168,8 +168,31 @@ class PaymentEntry(AccountsController):
|
||||
for reference in self.references:
|
||||
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
doc = frappe.get_doc(reference.reference_doctype, reference.reference_name)
|
||||
|
||||
repost_required = False
|
||||
for adv_reference in doc.get("advances"):
|
||||
if adv_reference.exchange_gain_loss != 0:
|
||||
repost_required = True
|
||||
break
|
||||
if repost_required:
|
||||
for item in doc.get("items"):
|
||||
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Linked Invoice {0} has Exchange Gain/Loss GL entries due to this Payment. Submit a Journal manually to reverse its effects."
|
||||
).format(get_link_to_form(doc.doctype, doc.name))
|
||||
)
|
||||
repost_required = False
|
||||
|
||||
doc.delink_advance_entries(self.name)
|
||||
|
||||
if repost_required:
|
||||
doc.reload()
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries()
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries()
|
||||
|
||||
def set_missing_values(self):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
for field in (
|
||||
|
||||
@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
bold_extra_batch_qty_needed = frappe.bold(
|
||||
abs(available_batch_qty - reserved_batch_qty - item.qty)
|
||||
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
|
||||
)
|
||||
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||
|
||||
@@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
|
||||
).format(item.idx, bold_invalid_batch_no, bold_item_name),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
|
||||
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
|
||||
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.qty):
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
@@ -652,7 +652,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
max_available_bundles = available_qty / item.stock_qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
|
||||
@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
|
||||
class ProcessStatementOfAccounts(Document):
|
||||
def validate(self):
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.name }}"
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||
|
||||
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
@@ -155,7 +156,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
||||
]
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[[fields_dict[customer_collection], "IN", selected]],
|
||||
)
|
||||
|
||||
@@ -178,7 +179,7 @@ def get_customers_based_on_sales_person(sales_person):
|
||||
if sales_person_records.get("Customer"):
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
||||
)
|
||||
else:
|
||||
@@ -227,7 +228,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if customer_collection == "Sales Partner":
|
||||
customers = frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["default_sales_partner", "=", collection_name]],
|
||||
)
|
||||
else:
|
||||
@@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
continue
|
||||
|
||||
customer_list.append(
|
||||
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
|
||||
{
|
||||
"name": customer.name,
|
||||
"customer_name": customer.customer_name,
|
||||
"primary_email": primary_email,
|
||||
"billing_email": billing_email,
|
||||
}
|
||||
)
|
||||
return customer_list
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_workflow": 1,
|
||||
"creation": "2020-08-03 16:35:21.852178",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"customer_name",
|
||||
"billing_email",
|
||||
"primary_email"
|
||||
],
|
||||
@@ -30,11 +30,18 @@
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 22:55:38.875601",
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
@@ -43,5 +50,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1078,7 +1078,7 @@ var select_loyalty_program = function(frm, loyalty_programs) {
|
||||
]
|
||||
});
|
||||
|
||||
dialog.set_primary_action(__("Set"), function() {
|
||||
dialog.set_primary_action(__("Set Loyalty Program"), function() {
|
||||
dialog.hide();
|
||||
return frappe.call({
|
||||
method: "frappe.client.set_value",
|
||||
|
||||
@@ -3470,6 +3470,78 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
def test_gain_loss_on_advance_cancellation(self):
|
||||
unlink_enabled = frappe.db.get_single_value(
|
||||
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
|
||||
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Receive",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer USD",
|
||||
"company": "_Test Company",
|
||||
"paid_from_account_currency": "USD",
|
||||
"paid_to_account_currency": "INR",
|
||||
"source_exchange_rate": 70,
|
||||
"target_exchange_rate": 1,
|
||||
"reference_no": "1",
|
||||
"reference_date": nowdate(),
|
||||
"received_amount": 70,
|
||||
"paid_amount": 1,
|
||||
"paid_from": "_Test Receivable USD - _TC",
|
||||
"paid_to": "_Test Cash - _TC",
|
||||
}
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=75,
|
||||
do_not_save=1,
|
||||
rate=1,
|
||||
)
|
||||
si = si.save()
|
||||
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": pe.name,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": 70,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
expected_gle = [
|
||||
["_Test Receivable USD - _TC", 75.0, 5.0],
|
||||
["Exchange Gain/Loss - _TC", 5.0, 0.0],
|
||||
["Sales - _TC", 0.0, 75.0],
|
||||
]
|
||||
check_gl_entries(self, si.name, expected_gle, nowdate())
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
expected_gle_after = [
|
||||
["_Test Receivable USD - _TC", 75.0, 0.0],
|
||||
["Sales - _TC", 0.0, 75.0],
|
||||
]
|
||||
check_gl_entries(self, si.name, expected_gle_after, nowdate())
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_sales_invoice_return(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
|
||||
for data in [asset_data, liability_data, equity_data]:
|
||||
if data:
|
||||
account_name = get_root_account_name(data[0].root_type, company)
|
||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||
if account_name:
|
||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||
|
||||
opening_balance[company] = opening_value
|
||||
|
||||
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
|
||||
|
||||
|
||||
def get_root_account_name(root_type, company):
|
||||
return frappe.get_all(
|
||||
root_account = frappe.get_all(
|
||||
"Account",
|
||||
fields=["account_name"],
|
||||
filters={
|
||||
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
|
||||
"parent_account": ("is", "not set"),
|
||||
},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
if root_account:
|
||||
return root_account[0][0]
|
||||
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
@@ -38,8 +38,11 @@
|
||||
{% if(data[i].posting_date) { %}
|
||||
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
|
||||
<td>{%= data[i].voucher_type %}
|
||||
<br>{%= data[i].voucher_no %}</td>
|
||||
<td>
|
||||
<br>{%= data[i].voucher_no %}
|
||||
</td>
|
||||
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
|
||||
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
|
||||
<span>
|
||||
{% if(!(filters.party || filters.account)) { %}
|
||||
{%= data[i].party || data[i].account %}
|
||||
<br>
|
||||
@@ -49,11 +52,14 @@
|
||||
{% if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}</td>
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
@@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"default": "",
|
||||
"fieldtype": "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.query_builder import Order
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -364,6 +365,7 @@ def get_column_names():
|
||||
|
||||
class GrossProfitGenerator(object):
|
||||
def __init__(self, filters=None):
|
||||
self.sle = {}
|
||||
self.data = []
|
||||
self.average_buying_rate = {}
|
||||
self.filters = frappe._dict(filters)
|
||||
@@ -373,7 +375,6 @@ class GrossProfitGenerator(object):
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_stock_ledger_entries()
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
self.get_returned_invoice_items()
|
||||
@@ -465,7 +466,14 @@ class GrossProfitGenerator(object):
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
row.qty += flt(returned_item_row.qty)
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
@@ -563,7 +571,7 @@ class GrossProfitGenerator(object):
|
||||
return flt(row.qty) * item_rate
|
||||
|
||||
else:
|
||||
my_sle = self.sle.get((item_code, row.warehouse))
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
if (row.update_stock or row.dn_detail) and my_sle:
|
||||
parenttype, parent = row.parenttype, row.parent
|
||||
if row.dn_detail:
|
||||
@@ -581,7 +589,7 @@ class GrossProfitGenerator(object):
|
||||
dn["item_row"],
|
||||
dn["warehouse"],
|
||||
)
|
||||
my_sle = self.sle.get((item_code, warehouse))
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
@@ -597,15 +605,12 @@ class GrossProfitGenerator(object):
|
||||
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
delivery_note = frappe.qb.DocType("Delivery Note")
|
||||
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(delivery_note)
|
||||
.inner_join(delivery_note_item)
|
||||
.on(delivery_note.name == delivery_note_item.parent)
|
||||
frappe.qb.from_(delivery_note_item)
|
||||
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
|
||||
.where(delivery_note.docstatus == 1)
|
||||
.where(delivery_note_item.docstatus == 1)
|
||||
.where(delivery_note_item.item_code == item_code)
|
||||
.where(delivery_note_item.against_sales_order == sales_order)
|
||||
.where(delivery_note_item.so_detail == so_detail)
|
||||
@@ -667,6 +672,19 @@ class GrossProfitGenerator(object):
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
||||
|
||||
if self.filters.sales_person:
|
||||
conditions += """
|
||||
and exists(select 1
|
||||
from `tabSales Team` st
|
||||
where st.parent = `tabSales Invoice`.name
|
||||
and st.sales_person = %(sales_person)s)
|
||||
"""
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
@@ -840,24 +858,36 @@ class GrossProfitGenerator(object):
|
||||
"Item", item_code, ["item_name", "description", "item_group", "brand"]
|
||||
)
|
||||
|
||||
def load_stock_ledger_entries(self):
|
||||
res = frappe.db.sql(
|
||||
"""select item_code, voucher_type, voucher_no,
|
||||
voucher_detail_no, stock_value, warehouse, actual_qty as qty
|
||||
from `tabStock Ledger Entry`
|
||||
where company=%(company)s and is_cancelled = 0
|
||||
order by
|
||||
item_code desc, warehouse desc, posting_date desc,
|
||||
posting_time desc, creation desc""",
|
||||
self.filters,
|
||||
as_dict=True,
|
||||
)
|
||||
self.sle = {}
|
||||
for r in res:
|
||||
if (r.item_code, r.warehouse) not in self.sle:
|
||||
self.sle[(r.item_code, r.warehouse)] = []
|
||||
def get_stock_ledger_entries(self, item_code, warehouse):
|
||||
if item_code and warehouse:
|
||||
if (item_code, warehouse) not in self.sle:
|
||||
sle = qb.DocType("Stock Ledger Entry")
|
||||
res = (
|
||||
qb.from_(sle)
|
||||
.select(
|
||||
sle.item_code,
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.voucher_detail_no,
|
||||
sle.stock_value,
|
||||
sle.warehouse,
|
||||
sle.actual_qty.as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(sle.company == self.filters.company)
|
||||
& (sle.item_code == item_code)
|
||||
& (sle.warehouse == warehouse)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.orderby(sle.item_code)
|
||||
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
self.sle[(r.item_code, r.warehouse)].append(r)
|
||||
self.sle[(item_code, warehouse)] = res
|
||||
|
||||
return self.sle[(item_code, warehouse)]
|
||||
return []
|
||||
|
||||
def load_product_bundle(self):
|
||||
self.product_bundles = {}
|
||||
|
||||
@@ -380,3 +380,82 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
# Create Credit Note for Invoice
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
expected_entry = {
|
||||
"parent_invoice": sinv.name,
|
||||
"currency": "INR",
|
||||
"sales_invoice": self.item,
|
||||
"customer": self.customer,
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
self.assertEqual(len(gp_entry), 2)
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[1])
|
||||
|
||||
def test_standalone_cr_notes(self):
|
||||
"""
|
||||
Standalone cr notes will be reported as usual
|
||||
"""
|
||||
# Make Cr Note
|
||||
sinv = self.create_sales_invoice(
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
expected_entry = {
|
||||
"parent_invoice": sinv.name,
|
||||
"currency": "INR",
|
||||
"sales_invoice": self.item,
|
||||
"customer": self.customer,
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": -1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
@@ -78,7 +78,6 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||
|
||||
@@ -118,12 +117,10 @@ def get_data(filters):
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
)
|
||||
|
||||
total_row = calculate_values(
|
||||
accounts, gl_entries_by_account, opening_balances, filters, company_currency
|
||||
)
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
@@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
return opening
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
"closing_credit": 0.0,
|
||||
}
|
||||
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
d.update(init.copy())
|
||||
|
||||
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
|
||||
prepare_opening_closing(d)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
return total_row
|
||||
|
||||
@@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
||||
accounts_by_name[d.parent_account][key] += d[key]
|
||||
|
||||
|
||||
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
|
||||
def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
@@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
||||
@@ -296,10 +296,6 @@ frappe.ui.form.on('Asset', {
|
||||
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
|
||||
},
|
||||
|
||||
opening_accumulated_depreciation: function(frm) {
|
||||
erpnext.asset.set_accumulated_depreciation(frm);
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
if (frm.doc.finance_books) {
|
||||
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
|
||||
@@ -519,19 +515,23 @@ frappe.ui.form.on('Depreciation Schedule', {
|
||||
},
|
||||
|
||||
depreciation_amount: function(frm, cdt, cdn) {
|
||||
erpnext.asset.set_accumulated_depreciation(frm);
|
||||
erpnext.asset.set_accumulated_depreciation(frm, locals[cdt][cdn].finance_book_id);
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
erpnext.asset.set_accumulated_depreciation = function(frm) {
|
||||
if(frm.doc.depreciation_method != "Manual") return;
|
||||
erpnext.asset.set_accumulated_depreciation = function(frm, finance_book_id) {
|
||||
var depreciation_method = frm.doc.finance_books[Number(finance_book_id) - 1].depreciation_method;
|
||||
|
||||
if(depreciation_method != "Manual") return;
|
||||
|
||||
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
|
||||
|
||||
$.each(frm.doc.schedules || [], function(i, row) {
|
||||
accumulated_depreciation += flt(row.depreciation_amount);
|
||||
frappe.model.set_value(row.doctype, row.name,
|
||||
"accumulated_depreciation_amount", accumulated_depreciation);
|
||||
if (row.finance_book_id === finance_book_id) {
|
||||
accumulated_depreciation += flt(row.depreciation_amount);
|
||||
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -84,14 +84,55 @@ class Asset(AccountsController):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
if self.should_prepare_depreciation_schedule():
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
def should_prepare_depreciation_schedule(self):
|
||||
if not self.get("schedules"):
|
||||
return True
|
||||
|
||||
old_asset_doc = self.get_doc_before_save()
|
||||
|
||||
if not old_asset_doc:
|
||||
return True
|
||||
|
||||
have_asset_details_been_modified = (
|
||||
old_asset_doc.gross_purchase_amount != self.gross_purchase_amount
|
||||
or old_asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
|
||||
or old_asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
|
||||
)
|
||||
|
||||
if have_asset_details_been_modified:
|
||||
return True
|
||||
|
||||
manual_fb_idx = -1
|
||||
for d in self.finance_books:
|
||||
if d.depreciation_method == "Manual":
|
||||
manual_fb_idx = d.idx - 1
|
||||
|
||||
no_manual_depr_or_have_manual_depr_details_been_modified = (
|
||||
manual_fb_idx == -1
|
||||
or old_asset_doc.finance_books[manual_fb_idx].total_number_of_depreciations
|
||||
!= self.finance_books[manual_fb_idx].total_number_of_depreciations
|
||||
or old_asset_doc.finance_books[manual_fb_idx].frequency_of_depreciation
|
||||
!= self.finance_books[manual_fb_idx].frequency_of_depreciation
|
||||
or old_asset_doc.finance_books[manual_fb_idx].depreciation_start_date
|
||||
!= getdate(self.finance_books[manual_fb_idx].depreciation_start_date)
|
||||
or old_asset_doc.finance_books[manual_fb_idx].expected_value_after_useful_life
|
||||
!= self.finance_books[manual_fb_idx].expected_value_after_useful_life
|
||||
)
|
||||
|
||||
if no_manual_depr_or_have_manual_depr_details_been_modified:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def validate_item(self):
|
||||
item = frappe.get_cached_value(
|
||||
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1
|
||||
@@ -225,9 +266,7 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_disposal):
|
||||
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
|
||||
"schedules"
|
||||
):
|
||||
if not self.get("schedules"):
|
||||
self.schedules = []
|
||||
|
||||
if not self.available_for_use_date:
|
||||
@@ -545,9 +584,7 @@ class Asset(AccountsController):
|
||||
def set_accumulated_depreciation(
|
||||
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
|
||||
):
|
||||
straight_line_idx = [
|
||||
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
|
||||
]
|
||||
straight_line_idx = []
|
||||
finance_books = []
|
||||
|
||||
for i, d in enumerate(self.get("schedules")):
|
||||
@@ -555,6 +592,12 @@ class Asset(AccountsController):
|
||||
continue
|
||||
|
||||
if int(d.finance_book_id) not in finance_books:
|
||||
straight_line_idx = [
|
||||
s.idx
|
||||
for s in self.get("schedules")
|
||||
if s.finance_book_id == d.finance_book_id
|
||||
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
|
||||
]
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
value_after_depreciation = flt(
|
||||
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
|
||||
@@ -853,10 +896,19 @@ class Asset(AccountsController):
|
||||
return 200.0 / args.get("total_number_of_depreciations")
|
||||
|
||||
if args.get("depreciation_method") == "Written Down Value":
|
||||
if args.get("rate_of_depreciation") and on_validate:
|
||||
if (
|
||||
args.get("rate_of_depreciation")
|
||||
and on_validate
|
||||
and not self.flags.increase_in_asset_value_due_to_repair
|
||||
):
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
if self.flags.increase_in_asset_value_due_to_repair:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(
|
||||
args.get("value_after_depreciation")
|
||||
)
|
||||
else:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
@@ -1128,17 +1180,21 @@ def get_total_days(date, frequency):
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
else:
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
else:
|
||||
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@ class AssetRepair(AccountsController):
|
||||
def before_submit(self):
|
||||
self.check_repair_status()
|
||||
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||
|
||||
self.increase_asset_value()
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
@@ -49,10 +53,7 @@ class AssetRepair(AccountsController):
|
||||
if self.get("capitalize_repair_cost"):
|
||||
self.make_gl_entries()
|
||||
|
||||
if (
|
||||
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
|
||||
and self.increase_in_asset_life
|
||||
):
|
||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||
self.modify_depreciation_schedule()
|
||||
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
@@ -62,7 +63,11 @@ class AssetRepair(AccountsController):
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||
|
||||
self.decrease_asset_value()
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
@@ -72,10 +77,7 @@ class AssetRepair(AccountsController):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.make_gl_entries(cancel=True)
|
||||
|
||||
if (
|
||||
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
|
||||
and self.increase_in_asset_life
|
||||
):
|
||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||
self.revert_depreciation_schedule_on_cancellation()
|
||||
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"over_order_allowance",
|
||||
"maintain_same_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
@@ -42,57 +43,6 @@
|
||||
"label": "Default Buying Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_multiple_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Item To Be Added Multiple Times in a Transaction"
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontract",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subcontract"
|
||||
},
|
||||
{
|
||||
"default": "Material Transferred for Subcontract",
|
||||
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Backflush Raw Materials of Subcontract Based On",
|
||||
"options": "BOM\nMaterial Transferred for Subcontract"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"",
|
||||
"description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.",
|
||||
"fieldname": "over_transfer_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Transfer Allowance (%)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_rate",
|
||||
@@ -110,12 +60,70 @@
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_multiple_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Item To Be Added Multiple Times in a Transaction"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontract",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subcontract"
|
||||
},
|
||||
{
|
||||
"default": "Material Transferred for Subcontract",
|
||||
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Backflush Raw Materials of Subcontract Based On",
|
||||
"options": "BOM\nMaterial Transferred for Subcontract"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"",
|
||||
"description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.",
|
||||
"fieldname": "over_transfer_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Transfer Allowance (%)"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -123,7 +131,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-08 19:26:23.548837",
|
||||
"modified": "2023-03-22 13:01:49.640869",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||
@@ -72,6 +75,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.validate_bom_for_subcontracting_items()
|
||||
self.create_raw_materials_supplied("supplied_items")
|
||||
self.set_received_qty_for_drop_ship_items()
|
||||
validate_against_blanket_order(self)
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.supplier, self.company, self.inter_company_order_reference
|
||||
)
|
||||
@@ -640,7 +644,7 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
||||
}
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
stock_entry.set_missing_values()
|
||||
stock_entry.set_missing_values(raise_error_if_no_rate=False)
|
||||
return stock_entry.as_dict()
|
||||
else:
|
||||
frappe.throw(_("No Items selected for transfer"))
|
||||
|
||||
@@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
|
||||
// custom buttons
|
||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||
frappe.set_route('query-report', 'General Ledger',
|
||||
{ party_type: 'Supplier', party: frm.doc.name });
|
||||
{ party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name });
|
||||
}, __("View"));
|
||||
|
||||
frm.add_custom_button(__('Accounts Payable'), function () {
|
||||
|
||||
@@ -128,18 +128,9 @@ class Supplier(TransactionBase):
|
||||
|
||||
def on_trash(self):
|
||||
if self.supplier_primary_contact:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSupplier`
|
||||
SET
|
||||
supplier_primary_contact=null,
|
||||
supplier_primary_address=null,
|
||||
mobile_no=null,
|
||||
email_id=null,
|
||||
primary_address=null
|
||||
WHERE name=%(name)s""",
|
||||
{"name": self.name},
|
||||
)
|
||||
self.db_set("supplier_primary_contact", None)
|
||||
if self.supplier_primary_address:
|
||||
self.db_set("supplier_primary_address", None)
|
||||
|
||||
delete_contact_and_address("Supplier", self.name)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
@@ -151,6 +152,44 @@ class TestSupplier(FrappeTestCase):
|
||||
# Rollback
|
||||
address.delete()
|
||||
|
||||
def test_serach_fields_for_supplier(self):
|
||||
from erpnext.controllers.queries import supplier_query
|
||||
|
||||
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Naming Series")
|
||||
|
||||
supplier_name = create_supplier(supplier_name="Test Supplier 1").name
|
||||
|
||||
make_property_setter(
|
||||
"Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype"
|
||||
)
|
||||
|
||||
data = supplier_query(
|
||||
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, supplier_name)
|
||||
self.assertEqual(data[0].supplier_group, "Services")
|
||||
self.assertTrue("supplier_type" not in data[0])
|
||||
|
||||
make_property_setter(
|
||||
"Supplier",
|
||||
None,
|
||||
"search_fields",
|
||||
"supplier_group, supplier_type",
|
||||
"Data",
|
||||
for_doctype="Doctype",
|
||||
)
|
||||
data = supplier_query(
|
||||
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, supplier_name)
|
||||
self.assertEqual(data[0].supplier_group, "Services")
|
||||
self.assertEqual(data[0].supplier_type, "Company")
|
||||
self.assertTrue("supplier_type" in data[0])
|
||||
|
||||
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Supplier Name")
|
||||
|
||||
|
||||
def create_supplier(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -78,18 +78,16 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Customer"
|
||||
conditions = []
|
||||
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
|
||||
|
||||
if cust_master_name == "Customer Name":
|
||||
fields = ["name", "customer_group", "territory"]
|
||||
else:
|
||||
fields = ["name", "customer_name", "customer_group", "territory"]
|
||||
fields = ["name"]
|
||||
if cust_master_name != "Customer Name":
|
||||
fields.append("customer_name")
|
||||
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
@@ -112,20 +110,20 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
as_dict=as_dict,
|
||||
)
|
||||
|
||||
|
||||
# searches for supplier
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Supplier"
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
|
||||
if supp_master_name == "Supplier Name":
|
||||
fields = ["name", "supplier_group"]
|
||||
else:
|
||||
fields = ["name", "supplier_name", "supplier_group"]
|
||||
fields = ["name"]
|
||||
if supp_master_name != "Supplier Name":
|
||||
fields.append("supplier_name")
|
||||
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
@@ -145,6 +143,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
as_dict=as_dict,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ def validate_returned_items(doc):
|
||||
)
|
||||
|
||||
elif ref.serial_no:
|
||||
if not d.serial_no:
|
||||
if d.qty and not d.serial_no:
|
||||
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
|
||||
else:
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
@@ -301,7 +301,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
||||
|
||||
# Used retrun against and supplier and is_retrun because there is an index added for it
|
||||
data = frappe.db.get_list(
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=[
|
||||
@@ -393,6 +393,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
if serial_nos:
|
||||
target_doc.serial_no = "\n".join(serial_nos)
|
||||
|
||||
if source_doc.get("rejected_serial_no"):
|
||||
returned_serial_nos = get_returned_serial_nos(
|
||||
source_doc, source_parent, serial_no_field="rejected_serial_no"
|
||||
)
|
||||
rejected_serial_nos = list(
|
||||
set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
|
||||
)
|
||||
if rejected_serial_nos:
|
||||
target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
|
||||
|
||||
if doctype == "Purchase Receipt":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
@@ -587,7 +597,7 @@ def get_filters(
|
||||
return filters
|
||||
|
||||
|
||||
def get_returned_serial_nos(child_doc, parent_doc):
|
||||
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||
@@ -596,7 +606,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
|
||||
serial_nos = []
|
||||
|
||||
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
|
||||
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
|
||||
|
||||
filters = [
|
||||
[parent_doc.doctype, "return_against", "=", parent_doc.name],
|
||||
@@ -606,6 +616,6 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
]
|
||||
|
||||
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||
serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
|
||||
|
||||
return serial_nos
|
||||
|
||||
@@ -450,7 +450,7 @@ class StatusUpdater(Document):
|
||||
ifnull((select
|
||||
ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
|
||||
/ sum(abs(%(target_ref_field)s)) * 100
|
||||
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
%(update_modified)s
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
|
||||
@@ -90,6 +90,7 @@ class LeaveAllocation(Document):
|
||||
if self.carry_forward:
|
||||
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
def on_update_after_submit(self):
|
||||
if self.has_value_changed("new_leaves_allocated"):
|
||||
self.validate_against_leave_applications()
|
||||
@@ -99,7 +100,11 @@ class LeaveAllocation(Document):
|
||||
# run required validations again since total leaves are being updated
|
||||
self.validate_leave_days_and_dates()
|
||||
|
||||
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
||||
leaves_to_be_added = flt(
|
||||
(self.new_leaves_allocated - self.get_existing_leave_count()),
|
||||
self.precision("new_leaves_allocated"),
|
||||
)
|
||||
|
||||
args = {
|
||||
"leaves": leaves_to_be_added,
|
||||
"from_date": self.from_date,
|
||||
@@ -118,14 +123,13 @@ class LeaveAllocation(Document):
|
||||
"employee": self.employee,
|
||||
"company": self.company,
|
||||
"leave_type": self.leave_type,
|
||||
"is_carry_forward": 0,
|
||||
"docstatus": 1,
|
||||
},
|
||||
pluck="leaves",
|
||||
fields=["SUM(leaves) as total_leaves"],
|
||||
)
|
||||
total_existing_leaves = 0
|
||||
for entry in ledger_entries:
|
||||
total_existing_leaves += entry
|
||||
|
||||
return total_existing_leaves
|
||||
return ledger_entries[0].total_leaves if ledger_entries else 0
|
||||
|
||||
def validate_against_leave_applications(self):
|
||||
leaves_taken = get_approved_leaves_for_period(
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Leave Period")
|
||||
frappe.db.delete("Leave Allocation")
|
||||
frappe.db.delete("Leave Ledger Entry")
|
||||
|
||||
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
|
||||
self.employee = frappe.get_doc("Employee", emp_id)
|
||||
@@ -69,7 +70,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
|
||||
def test_validation_for_over_allocation(self):
|
||||
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
|
||||
leave_type.save()
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
@@ -137,9 +137,9 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
)
|
||||
).insert()
|
||||
|
||||
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
||||
leave_type.max_leaves_allowed = 25
|
||||
leave_type.save()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=25
|
||||
)
|
||||
|
||||
# 15 leaves allocated in this period
|
||||
allocation = create_leave_allocation(
|
||||
@@ -174,9 +174,9 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
)
|
||||
).insert()
|
||||
|
||||
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
||||
leave_type.max_leaves_allowed = 30
|
||||
leave_type.save()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=30
|
||||
)
|
||||
|
||||
# 15 leaves allocated
|
||||
allocation = create_leave_allocation(
|
||||
@@ -207,7 +207,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
|
||||
def test_validate_back_dated_allocation_update(self):
|
||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||
leave_type.save()
|
||||
|
||||
# initial leave allocation = 15
|
||||
leave_allocation = create_leave_allocation(
|
||||
@@ -235,10 +234,12 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
self.assertRaises(BackDatedAllocationError, leave_allocation.save)
|
||||
|
||||
def test_carry_forward_calculation(self):
|
||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||
leave_type.maximum_carry_forwarded_leaves = 10
|
||||
leave_type.max_leaves_allowed = 30
|
||||
leave_type.save()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave",
|
||||
is_carry_forward=1,
|
||||
maximum_carry_forwarded_leaves=10,
|
||||
max_leaves_allowed=30,
|
||||
)
|
||||
|
||||
# initial leave allocation = 15
|
||||
leave_allocation = create_leave_allocation(
|
||||
@@ -286,7 +287,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
leave_type.save()
|
||||
|
||||
# initial leave allocation
|
||||
leave_allocation = create_leave_allocation(
|
||||
@@ -352,12 +352,51 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
)
|
||||
leave_allocation.submit()
|
||||
leave_allocation.reload()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
|
||||
|
||||
leave_allocation.new_leaves_allocated = 40
|
||||
leave_allocation.submit()
|
||||
leave_allocation.save()
|
||||
leave_allocation.reload()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||
|
||||
updated_entry = frappe.db.get_all(
|
||||
"Leave Ledger Entry",
|
||||
{"transaction_name": leave_allocation.name},
|
||||
pluck="leaves",
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(updated_entry[0], 25)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 40)
|
||||
|
||||
def test_leave_addition_after_submit_with_carry_forward(self):
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
create_carry_forwarded_allocation,
|
||||
)
|
||||
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
include_holiday=True,
|
||||
)
|
||||
|
||||
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
|
||||
# 15 new leaves, 15 carry forwarded leaves
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
|
||||
|
||||
leave_allocation.new_leaves_allocated = 32
|
||||
leave_allocation.save()
|
||||
leave_allocation.reload()
|
||||
|
||||
updated_entry = frappe.db.get_all(
|
||||
"Leave Ledger Entry",
|
||||
{"transaction_name": leave_allocation.name},
|
||||
pluck="leaves",
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
self.assertEqual(updated_entry[0], 17)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 47)
|
||||
|
||||
def test_leave_subtraction_after_submit(self):
|
||||
leave_allocation = create_leave_allocation(
|
||||
@@ -365,12 +404,49 @@ class TestLeaveAllocation(FrappeTestCase):
|
||||
)
|
||||
leave_allocation.submit()
|
||||
leave_allocation.reload()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
|
||||
|
||||
leave_allocation.new_leaves_allocated = 10
|
||||
leave_allocation.submit()
|
||||
leave_allocation.reload()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||
|
||||
updated_entry = frappe.db.get_all(
|
||||
"Leave Ledger Entry",
|
||||
{"transaction_name": leave_allocation.name},
|
||||
pluck="leaves",
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(updated_entry[0], -5)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 10)
|
||||
|
||||
def test_leave_subtraction_after_submit_with_carry_forward(self):
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
create_carry_forwarded_allocation,
|
||||
)
|
||||
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
include_holiday=True,
|
||||
)
|
||||
|
||||
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
|
||||
|
||||
leave_allocation.new_leaves_allocated = 8
|
||||
leave_allocation.save()
|
||||
|
||||
updated_entry = frappe.db.get_all(
|
||||
"Leave Ledger Entry",
|
||||
{"transaction_name": leave_allocation.name},
|
||||
pluck="leaves",
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
self.assertEqual(updated_entry[0], -7)
|
||||
self.assertEqual(leave_allocation.total_leaves_allocated, 23)
|
||||
|
||||
def test_validation_against_leave_application_after_submit(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
@@ -817,7 +817,9 @@ def get_leave_balance_on(
|
||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
||||
|
||||
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
|
||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||
cf_expiry = get_allocation_expiry_for_cf_leaves(
|
||||
employee, leave_type, to_date, allocation.from_date
|
||||
)
|
||||
|
||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||
|
||||
@@ -832,6 +834,7 @@ def get_leave_balance_on(
|
||||
def get_leave_allocation_records(employee, date, leave_type=None):
|
||||
"""Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
|
||||
Ledger = frappe.qb.DocType("Leave Ledger Entry")
|
||||
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||
|
||||
cf_leave_case = (
|
||||
frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
|
||||
@@ -845,21 +848,33 @@ def get_leave_allocation_records(employee, date, leave_type=None):
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Ledger)
|
||||
.inner_join(LeaveAllocation)
|
||||
.on(Ledger.transaction_name == LeaveAllocation.name)
|
||||
.select(
|
||||
sum_cf_leaves,
|
||||
sum_new_leaves,
|
||||
Min(Ledger.from_date).as_("from_date"),
|
||||
Max(Ledger.to_date).as_("to_date"),
|
||||
Ledger.leave_type,
|
||||
Ledger.employee,
|
||||
)
|
||||
.where(
|
||||
(Ledger.from_date <= date)
|
||||
& (Ledger.to_date >= date)
|
||||
& (Ledger.docstatus == 1)
|
||||
& (Ledger.transaction_type == "Leave Allocation")
|
||||
& (Ledger.employee == employee)
|
||||
& (Ledger.is_expired == 0)
|
||||
& (Ledger.is_lwp == 0)
|
||||
& (
|
||||
# newly allocated leave's end date is same as the leave allocation's to date
|
||||
((Ledger.is_carry_forward == 0) & (Ledger.to_date >= date))
|
||||
# carry forwarded leave's end date won't be same as the leave allocation's to date
|
||||
# it's between the leave allocation's from and to date
|
||||
| (
|
||||
(Ledger.is_carry_forward == 1)
|
||||
& (Ledger.to_date.between(LeaveAllocation.from_date, LeaveAllocation.to_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -881,6 +896,7 @@ def get_leave_allocation_records(employee, date, leave_type=None):
|
||||
"unused_leaves": d.cf_leaves,
|
||||
"new_leaves_allocated": d.new_leaves,
|
||||
"leave_type": d.leave_type,
|
||||
"employee": d.employee,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -919,22 +935,51 @@ def get_remaining_leaves(
|
||||
|
||||
return remaining_leaves
|
||||
|
||||
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
|
||||
leaves_taken
|
||||
)
|
||||
|
||||
# balance for carry forwarded leaves
|
||||
if cf_expiry and allocation.unused_leaves:
|
||||
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
|
||||
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
|
||||
# allocation contains both carry forwarded and new leaves
|
||||
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(allocation, cf_expiry)
|
||||
|
||||
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
|
||||
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
|
||||
if getdate(date) > getdate(cf_expiry):
|
||||
# carry forwarded leaves have expired
|
||||
cf_leaves = remaining_cf_leaves = 0
|
||||
else:
|
||||
cf_leaves = flt(allocation.unused_leaves) + flt(cf_leaves_taken)
|
||||
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
|
||||
|
||||
# new leaves allocated - new leaves taken + cf leave balance
|
||||
# Note: `new_leaves_taken` is added here because its already a -ve number in the ledger
|
||||
leave_balance = (flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)) + flt(cf_leaves)
|
||||
leave_balance_for_consumption = (
|
||||
flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)
|
||||
) + flt(remaining_cf_leaves)
|
||||
else:
|
||||
# allocation only contains newly allocated leaves
|
||||
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
|
||||
leaves_taken
|
||||
)
|
||||
|
||||
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
|
||||
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
|
||||
|
||||
|
||||
def get_new_and_cf_leaves_taken(allocation: Dict, cf_expiry: str) -> Tuple[float, float]:
|
||||
"""returns new leaves taken and carry forwarded leaves taken within an allocation period based on cf leave expiry"""
|
||||
cf_leaves_taken = get_leaves_for_period(
|
||||
allocation.employee, allocation.leave_type, allocation.from_date, cf_expiry
|
||||
)
|
||||
new_leaves_taken = get_leaves_for_period(
|
||||
allocation.employee, allocation.leave_type, add_days(cf_expiry, 1), allocation.to_date
|
||||
)
|
||||
|
||||
# using abs because leaves taken is a -ve number in the ledger
|
||||
if abs(cf_leaves_taken) > allocation.unused_leaves:
|
||||
# adjust the excess leaves in new_leaves_taken
|
||||
new_leaves_taken += -(abs(cf_leaves_taken) - allocation.unused_leaves)
|
||||
cf_leaves_taken = -allocation.unused_leaves
|
||||
|
||||
return new_leaves_taken, cf_leaves_taken
|
||||
|
||||
|
||||
def get_leaves_for_period(
|
||||
employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True
|
||||
) -> float:
|
||||
|
||||
@@ -28,6 +28,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
|
||||
get_leave_allocation_records,
|
||||
get_leave_balance_on,
|
||||
get_leave_details,
|
||||
get_new_and_cf_leaves_taken,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
@@ -96,6 +97,9 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
from_date = get_year_start(getdate())
|
||||
to_date = get_year_ending(getdate())
|
||||
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
||||
list_without_weekly_offs = make_holiday_list(
|
||||
"Holiday List w/o Weekly Offs", from_date=from_date, to_date=to_date, add_weekly_offs=False
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
|
||||
frappe.get_doc(
|
||||
@@ -699,7 +703,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
leave_type.insert()
|
||||
|
||||
create_carry_forwarded_allocation(employee, leave_type)
|
||||
details = get_leave_balance_on(
|
||||
@@ -771,7 +774,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
employee = get_employee()
|
||||
|
||||
leave_type = create_leave_type(leave_type_name="Test Leave Type 1")
|
||||
leave_type.save()
|
||||
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name
|
||||
@@ -814,7 +816,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
include_holiday=True,
|
||||
)
|
||||
leave_type.submit()
|
||||
|
||||
create_carry_forwarded_allocation(employee, leave_type)
|
||||
|
||||
@@ -853,7 +854,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
leave_type.submit()
|
||||
|
||||
create_carry_forwarded_allocation(employee, leave_type)
|
||||
|
||||
@@ -991,18 +991,169 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(leave_allocation, expected)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_leave_allocation_records(self):
|
||||
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||
def test_leave_details_with_expired_cf_leaves(self):
|
||||
"""Tests leave details:
|
||||
Case 1: All leaves available before cf leave expiry
|
||||
Case 2: Remaining Leaves after cf leave expiry
|
||||
"""
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
leave_type.insert()
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
|
||||
cf_expiry = frappe.db.get_value(
|
||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||
)
|
||||
|
||||
# case 1: all leaves available before cf leave expiry
|
||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, -1))
|
||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name]["remaining_leaves"], 30.0)
|
||||
|
||||
# case 2: cf leaves expired
|
||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 1))
|
||||
expected_data = {
|
||||
"total_leaves": 30.0,
|
||||
"expired_leaves": 15.0,
|
||||
"leaves_taken": 0.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 15.0,
|
||||
}
|
||||
|
||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||
|
||||
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||
def test_leave_details_with_application_across_cf_expiry(self):
|
||||
"""Tests leave details with leave application across cf expiry, such that:
|
||||
cf leaves are partially expired and partially consumed
|
||||
"""
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
cf_expiry = frappe.db.get_value(
|
||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||
)
|
||||
|
||||
# leave application across cf expiry
|
||||
application = make_leave_application(
|
||||
employee.name,
|
||||
cf_expiry,
|
||||
add_days(cf_expiry, 3),
|
||||
leave_type.name,
|
||||
)
|
||||
|
||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||
expected_data = {
|
||||
"total_leaves": 30.0,
|
||||
"expired_leaves": 14.0,
|
||||
"leaves_taken": 4.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 12.0,
|
||||
}
|
||||
|
||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||
|
||||
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||
def test_leave_details_with_application_across_cf_expiry_2(self):
|
||||
"""Tests the same case as above but with leave days greater than cf leaves allocated"""
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
cf_expiry = frappe.db.get_value(
|
||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||
)
|
||||
|
||||
# leave application across cf expiry, 20 days leave
|
||||
application = make_leave_application(
|
||||
employee.name,
|
||||
add_days(cf_expiry, -16),
|
||||
add_days(cf_expiry, 3),
|
||||
leave_type.name,
|
||||
)
|
||||
|
||||
# 15 cf leaves and 5 new leaves should be consumed
|
||||
# after adjustment of the actual days breakup (17 and 3) because only 15 cf leaves have been allocated
|
||||
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(leave_alloc, cf_expiry)
|
||||
self.assertEqual(new_leaves_taken, -5.0)
|
||||
self.assertEqual(cf_leaves_taken, -15.0)
|
||||
|
||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||
expected_data = {
|
||||
"total_leaves": 30.0,
|
||||
"expired_leaves": 0,
|
||||
"leaves_taken": 20.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 10.0,
|
||||
}
|
||||
|
||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||
|
||||
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||
def test_leave_details_with_application_after_cf_expiry(self):
|
||||
"""Tests leave details with leave application after cf expiry, such that:
|
||||
cf leaves are completely expired and only newly allocated leaves are consumed
|
||||
"""
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
cf_expiry = frappe.db.get_value(
|
||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||
)
|
||||
|
||||
# leave application after cf expiry
|
||||
application = make_leave_application(
|
||||
employee.name,
|
||||
add_days(cf_expiry, 1),
|
||||
add_days(cf_expiry, 4),
|
||||
leave_type.name,
|
||||
)
|
||||
|
||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||
expected_data = {
|
||||
"total_leaves": 30.0,
|
||||
"expired_leaves": 15.0,
|
||||
"leaves_taken": 4.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 11.0,
|
||||
}
|
||||
|
||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_leave_allocation_records(self):
|
||||
"""Tests if total leaves allocated before and after carry forwarded leave expiry is same"""
|
||||
employee = get_employee()
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90,
|
||||
)
|
||||
|
||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||
cf_expiry = frappe.db.get_value(
|
||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||
)
|
||||
|
||||
# test total leaves allocated before cf leave expiry
|
||||
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, -1), leave_type.name)
|
||||
expected_data = {
|
||||
"from_date": getdate(leave_alloc.from_date),
|
||||
"to_date": getdate(leave_alloc.to_date),
|
||||
@@ -1010,9 +1161,15 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
"unused_leaves": 15.0,
|
||||
"new_leaves_allocated": 15.0,
|
||||
"leave_type": leave_type.name,
|
||||
"employee": employee.name,
|
||||
}
|
||||
self.assertEqual(details.get(leave_type.name), expected_data)
|
||||
|
||||
# test leaves allocated after carry forwarded leaves expiry, should be same thoroughout allocation period
|
||||
# cf leaves should show up under expired or taken leaves later
|
||||
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, 1), leave_type.name)
|
||||
self.assertEqual(details.get(leave_type.name), expected_data)
|
||||
|
||||
|
||||
def create_carry_forwarded_allocation(employee, leave_type):
|
||||
# initial leave allocation
|
||||
|
||||
@@ -9,7 +9,8 @@ test_records = frappe.get_test_records("Leave Type")
|
||||
def create_leave_type(**args):
|
||||
args = frappe._dict(args)
|
||||
if frappe.db.exists("Leave Type", args.leave_type_name):
|
||||
return frappe.get_doc("Leave Type", args.leave_type_name)
|
||||
frappe.delete_doc("Leave Type", args.leave_type_name, force=True)
|
||||
|
||||
leave_type = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Type",
|
||||
@@ -23,10 +24,14 @@ def create_leave_type(**args):
|
||||
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
|
||||
"encashment_threshold_days": args.encashment_threshold_days or 5,
|
||||
"earning_component": "Leave Encashment",
|
||||
"max_leaves_allowed": args.max_leaves_allowed,
|
||||
"maximum_carry_forwarded_leaves": args.maximum_carry_forwarded_leaves,
|
||||
}
|
||||
)
|
||||
|
||||
if leave_type.is_ppl:
|
||||
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
|
||||
|
||||
leave_type.insert()
|
||||
|
||||
return leave_type
|
||||
|
||||
@@ -154,7 +154,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
||||
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||
def test_opening_balance_considers_carry_forwarded_leaves(self):
|
||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1)
|
||||
leave_type.insert()
|
||||
|
||||
# 30 leaves allocated for first half of the year
|
||||
allocation1 = make_allocation_record(
|
||||
|
||||
@@ -64,8 +64,6 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "prevdoc_detail_docname.sales_person",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "service_person",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -110,13 +108,15 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-27 17:47:21.474282",
|
||||
"modified": "2023-02-27 11:09:33.114458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Maintenance",
|
||||
"name": "Maintenance Visit Purpose",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Purchase Order': 'Purchase Order',
|
||||
'Sales Order': 'Sales Order',
|
||||
'Quotation': 'Quotation',
|
||||
};
|
||||
|
||||
frm.add_fetch("customer", "customer_name", "customer_name");
|
||||
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
@@ -29,21 +30,23 @@ class BlanketOrder(Document):
|
||||
|
||||
def update_ordered_qty(self):
|
||||
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
|
||||
|
||||
trans = frappe.qb.DocType(ref_doctype)
|
||||
trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
|
||||
|
||||
item_ordered_qty = frappe._dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select trans_item.item_code, sum(trans_item.stock_qty) as qty
|
||||
from `tab{0} Item` trans_item, `tab{0}` trans
|
||||
where trans.name = trans_item.parent
|
||||
and trans_item.blanket_order=%s
|
||||
and trans.docstatus=1
|
||||
and trans.status not in ('Closed', 'Stopped')
|
||||
group by trans_item.item_code
|
||||
""".format(
|
||||
ref_doctype
|
||||
),
|
||||
self.name,
|
||||
)
|
||||
(
|
||||
frappe.qb.from_(trans_item)
|
||||
.from_(trans)
|
||||
.select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
|
||||
.where(
|
||||
(trans.name == trans_item.parent)
|
||||
& (trans_item.blanket_order == self.name)
|
||||
& (trans.docstatus == 1)
|
||||
& (trans.status.notin(["Stopped", "Closed"]))
|
||||
)
|
||||
.groupby(trans_item.item_code)
|
||||
).run()
|
||||
)
|
||||
|
||||
for d in self.items:
|
||||
@@ -79,7 +82,43 @@ def make_order(source_name):
|
||||
"doctype": doctype + " Item",
|
||||
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
return target_doc
|
||||
|
||||
|
||||
def validate_against_blanket_order(order_doc):
|
||||
if order_doc.doctype in ("Sales Order", "Purchase Order"):
|
||||
order_data = {}
|
||||
|
||||
for item in order_doc.get("items"):
|
||||
if item.against_blanket_order and item.blanket_order:
|
||||
if item.blanket_order in order_data:
|
||||
if item.item_code in order_data[item.blanket_order]:
|
||||
order_data[item.blanket_order][item.item_code] += item.qty
|
||||
else:
|
||||
order_data[item.blanket_order][item.item_code] = item.qty
|
||||
else:
|
||||
order_data[item.blanket_order] = {item.item_code: item.qty}
|
||||
|
||||
if order_data:
|
||||
allowance = flt(
|
||||
frappe.db.get_single_value(
|
||||
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
|
||||
"over_order_allowance",
|
||||
)
|
||||
)
|
||||
for bo_name, item_data in order_data.items():
|
||||
bo_doc = frappe.get_doc("Blanket Order", bo_name)
|
||||
for item in bo_doc.get("items"):
|
||||
if item.item_code in item_data:
|
||||
remaining_qty = item.qty - item.ordered_qty
|
||||
allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
|
||||
if allowed_qty < item_data[item.item_code]:
|
||||
frappe.throw(
|
||||
_("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
|
||||
item.item_code, allowed_qty, bo_name
|
||||
)
|
||||
)
|
||||
|
||||
@@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
|
||||
po1.currency = get_company_currency(po1.company)
|
||||
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
|
||||
|
||||
def test_over_order_allowance(self):
|
||||
# Sales Order
|
||||
bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
|
||||
|
||||
frappe.flags.args.doctype = "Sales Order"
|
||||
so = make_order(bo.name)
|
||||
so.currency = get_company_currency(so.company)
|
||||
so.delivery_date = today()
|
||||
so.items[0].qty = 110
|
||||
self.assertRaises(frappe.ValidationError, so.submit)
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
|
||||
so.submit()
|
||||
|
||||
# Purchase Order
|
||||
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
|
||||
|
||||
frappe.flags.args.doctype = "Purchase Order"
|
||||
po = make_order(bo.name)
|
||||
po.currency = get_company_currency(po.company)
|
||||
po.schedule_date = today()
|
||||
po.items[0].qty = 110
|
||||
self.assertRaises(frappe.ValidationError, po.submit)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
|
||||
po.submit()
|
||||
|
||||
|
||||
def make_blanket_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -31,7 +31,7 @@ class BOMTree:
|
||||
|
||||
# specifying the attributes to save resources
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
|
||||
|
||||
def __init__(
|
||||
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
|
||||
@@ -50,9 +50,10 @@ class BOMTree:
|
||||
def __create_tree(self):
|
||||
bom = frappe.get_cached_doc("BOM", self.name)
|
||||
self.item_code = bom.item
|
||||
self.bom_qty = bom.quantity
|
||||
|
||||
for item in bom.get("items", []):
|
||||
qty = item.qty / bom.quantity # quantity per unit
|
||||
qty = item.stock_qty / bom.quantity # quantity per unit
|
||||
exploded_qty = self.exploded_qty * qty
|
||||
if item.bom_no:
|
||||
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||
|
||||
@@ -536,7 +536,34 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||
from frappe.query_builder.functions import Sum
|
||||
def _get_job_card_items_transferred_qty(ste_doc):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
job_card_items_transferred_qty = {}
|
||||
job_card_items = [
|
||||
x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
|
||||
]
|
||||
|
||||
if job_card_items:
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sed)
|
||||
.join(se)
|
||||
.on(sed.parent == se.name)
|
||||
.select(sed.job_card_item, Sum(sed.qty))
|
||||
.where(
|
||||
(sed.job_card_item.isin(job_card_items))
|
||||
& (se.docstatus == 1)
|
||||
& (se.purpose == "Material Transfer for Manufacture")
|
||||
)
|
||||
.groupby(sed.job_card_item)
|
||||
)
|
||||
|
||||
job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
|
||||
|
||||
return job_card_items_transferred_qty
|
||||
|
||||
def _validate_over_transfer(row, transferred_qty):
|
||||
"Block over transfer of items if not allowed in settings."
|
||||
@@ -553,29 +580,23 @@ class JobCard(Document):
|
||||
exc=JobCardOverTransferError,
|
||||
)
|
||||
|
||||
for row in ste_doc.items:
|
||||
if not row.job_card_item:
|
||||
continue
|
||||
|
||||
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]
|
||||
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
|
||||
|
||||
if job_card_items_transferred_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))
|
||||
for row in ste_doc.items:
|
||||
if not row.job_card_item:
|
||||
continue
|
||||
|
||||
transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
|
||||
|
||||
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 in Job Card for which RM was transferred."
|
||||
|
||||
@@ -476,7 +476,7 @@ frappe.ui.form.on("Work Order Item", {
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
"required_qty": 1,
|
||||
"required_qty": row.required_qty || 1,
|
||||
"item_name": r.message.item_name,
|
||||
"description": r.message.description,
|
||||
"source_warehouse": r.message.default_warehouse,
|
||||
|
||||
@@ -690,7 +690,7 @@ class WorkOrder(Document):
|
||||
|
||||
for node in bom_traversal:
|
||||
if node.is_bom:
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
|
||||
|
||||
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
||||
|
||||
@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.id == "item") {
|
||||
if (data["enough_parts_to_build"] > 0) {
|
||||
if (data["in_stock_qty"] >= data["required_qty"]) {
|
||||
value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
|
||||
} else {
|
||||
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.utils import cint
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
@@ -34,57 +35,55 @@ def get_columns():
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
qty_to_produce = filters.get("qty_to_produce") or 1
|
||||
if int(qty_to_produce) < 0:
|
||||
frappe.throw(_("Quantity to Produce can not be less than Zero"))
|
||||
qty_to_produce = filters.get("qty_to_produce")
|
||||
if cint(qty_to_produce) <= 0:
|
||||
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType(bom_item_table)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bom)
|
||||
.inner_join(bom_item)
|
||||
.on(bom.name == bom_item.parent)
|
||||
.left_join(bin)
|
||||
.on(bom_item.item_code == bin.item_code)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
bom_item.description,
|
||||
bom_item.stock_qty,
|
||||
bom_item.stock_uom,
|
||||
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
|
||||
Sum(bin.actual_qty),
|
||||
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
BOM = frappe.qb.DocType("BOM")
|
||||
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||
BIN = frappe.qb.DocType("Bin")
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
CONDITIONS = ()
|
||||
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where(
|
||||
(wh.lft >= warehouse_details.lft)
|
||||
& (wh.rgt <= warehouse_details.rgt)
|
||||
& (bin.warehouse == wh.name)
|
||||
)
|
||||
)
|
||||
if warehouse_details:
|
||||
CONDITIONS = ExistsCriterion(
|
||||
frappe.qb.from_(WH)
|
||||
.select(WH.name)
|
||||
.where(
|
||||
(WH.lft >= warehouse_details.lft)
|
||||
& (WH.rgt <= warehouse_details.rgt)
|
||||
& (BIN.warehouse == WH.name)
|
||||
)
|
||||
else:
|
||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
||||
)
|
||||
else:
|
||||
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||
|
||||
return query.run()
|
||||
QUERY = (
|
||||
frappe.qb.from_(BOM)
|
||||
.inner_join(BOM_ITEM)
|
||||
.on(BOM.name == BOM_ITEM.parent)
|
||||
.left_join(BIN)
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
.groupby(BOM_ITEM.item_code)
|
||||
)
|
||||
|
||||
return QUERY.run()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.exceptions import ValidationError
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import floor
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
|
||||
get_bom_stock as bom_stock_report,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestBomStockReport(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.warehouse = "_Test Warehouse - _TC"
|
||||
self.fg_item, self.rm_items = create_items()
|
||||
make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
|
||||
make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
|
||||
self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
|
||||
|
||||
def test_bom_stock_report(self):
|
||||
# Test 1: When `qty_to_produce` is 0.
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty_to_produce": 0,
|
||||
}
|
||||
)
|
||||
self.assertRaises(ValidationError, bom_stock_report, filters)
|
||||
|
||||
# Test 2: When stock is not available.
|
||||
data = bom_stock_report(
|
||||
frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty_to_produce": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
# Test 3: When stock is available.
|
||||
data = bom_stock_report(
|
||||
frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": self.warehouse,
|
||||
"qty_to_produce": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
expected_data = get_expected_data(self.bom, self.warehouse, 1)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
|
||||
def create_items():
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 100,
|
||||
"opening_stock": 100,
|
||||
"last_purchase_rate": 100,
|
||||
}
|
||||
).name
|
||||
rm_item2 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 200,
|
||||
"opening_stock": 200,
|
||||
"last_purchase_rate": 200,
|
||||
}
|
||||
).name
|
||||
|
||||
return fg_item, [rm_item1, rm_item2]
|
||||
|
||||
|
||||
def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||
expected_data = []
|
||||
|
||||
for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
|
||||
in_stock_qty = None
|
||||
if frappe.db.exists("Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"):
|
||||
in_stock_qty = frappe.get_cached_value(
|
||||
"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
|
||||
)
|
||||
|
||||
expected_data.append(
|
||||
[
|
||||
item.item_code,
|
||||
item.description,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
item.stock_qty * qty_to_produce / bom.quantity,
|
||||
in_stock_qty,
|
||||
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
|
||||
if in_stock_qty
|
||||
else None,
|
||||
]
|
||||
)
|
||||
|
||||
return expected_data
|
||||
@@ -376,3 +376,4 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v13_0.update_docs_link
|
||||
|
||||
@@ -1,16 +1,61 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.regional.india.setup import make_custom_fields
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.get_all("Company", filters={"country": "India"}):
|
||||
frappe.reload_doc("accounts", "doctype", "POS Invoice")
|
||||
frappe.reload_doc("accounts", "doctype", "POS Invoice Item")
|
||||
|
||||
make_custom_fields()
|
||||
custom_fields = get_non_profit_custom_fields()
|
||||
create_custom_fields(custom_fields, update=True)
|
||||
|
||||
if not frappe.db.exists("Party Type", "Donor"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"}
|
||||
).insert(ignore_permissions=True)
|
||||
).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
||||
|
||||
def get_non_profit_custom_fields():
|
||||
return {
|
||||
"Company": [
|
||||
{
|
||||
"fieldname": "non_profit_section",
|
||||
"label": "Non Profit Settings",
|
||||
"fieldtype": "Section Break",
|
||||
"insert_after": "asset_received_but_not_billed",
|
||||
"collapsible": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "company_80g_number",
|
||||
"label": "80G Number",
|
||||
"fieldtype": "Data",
|
||||
"insert_after": "non_profit_section",
|
||||
},
|
||||
{
|
||||
"fieldname": "with_effect_from",
|
||||
"label": "80G With Effect From",
|
||||
"fieldtype": "Date",
|
||||
"insert_after": "company_80g_number",
|
||||
},
|
||||
{
|
||||
"fieldname": "pan_details",
|
||||
"label": "PAN Number",
|
||||
"fieldtype": "Data",
|
||||
"insert_after": "with_effect_from",
|
||||
},
|
||||
],
|
||||
"Member": [
|
||||
{
|
||||
"fieldname": "pan_number",
|
||||
"label": "PAN Details",
|
||||
"fieldtype": "Data",
|
||||
"insert_after": "email_id",
|
||||
},
|
||||
],
|
||||
"Donor": [
|
||||
{
|
||||
"fieldname": "pan_number",
|
||||
"label": "PAN Details",
|
||||
"fieldtype": "Data",
|
||||
"insert_after": "email",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
navbar_settings = frappe.get_single("Navbar Settings")
|
||||
for item in navbar_settings.help_dropdown:
|
||||
if item.is_standard and item.route == "https://erpnext.com/docs/user/manual":
|
||||
item.route = "https://docs.erpnext.com/docs/v13/user/manual/en/introduction"
|
||||
|
||||
navbar_settings.save()
|
||||
@@ -324,6 +324,8 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
|
||||
|
||||
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||
|
||||
if not cint(include_holidays_in_total_working_days):
|
||||
working_days -= len(holidays)
|
||||
working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays]
|
||||
@@ -335,10 +337,14 @@ class SalarySlip(TransactionBase):
|
||||
frappe.throw(_("Please set Payroll based on in Payroll settings"))
|
||||
|
||||
if payroll_based_on == "Attendance":
|
||||
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
|
||||
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(
|
||||
holidays, relieving_date
|
||||
)
|
||||
self.absent_days = absent
|
||||
else:
|
||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days_list)
|
||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(
|
||||
holidays, working_days_list, relieving_date
|
||||
)
|
||||
|
||||
if not lwp:
|
||||
lwp = actual_lwp
|
||||
@@ -461,7 +467,10 @@ class SalarySlip(TransactionBase):
|
||||
def get_holidays_for_employee(self, start_date, end_date):
|
||||
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
|
||||
|
||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days_list):
|
||||
def calculate_lwp_or_ppl_based_on_leave_application(
|
||||
self, holidays, working_days_list, relieving_date=None
|
||||
):
|
||||
|
||||
lwp = 0
|
||||
|
||||
daily_wages_fraction_for_half_day = (
|
||||
@@ -469,6 +478,9 @@ class SalarySlip(TransactionBase):
|
||||
)
|
||||
|
||||
for d in working_days_list:
|
||||
if relieving_date and getdate(d) > getdate(relieving_date):
|
||||
break
|
||||
|
||||
leave = get_lwp_or_ppl_for_date(d, self.employee, holidays)
|
||||
|
||||
if leave:
|
||||
@@ -488,10 +500,15 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
return lwp
|
||||
|
||||
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
|
||||
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays, relieving_date=None):
|
||||
lwp = 0
|
||||
absent = 0
|
||||
|
||||
end_date = self.end_date
|
||||
|
||||
if relieving_date:
|
||||
end_date = relieving_date
|
||||
|
||||
daily_wages_fraction_for_half_day = (
|
||||
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
||||
)
|
||||
@@ -506,7 +523,7 @@ class SalarySlip(TransactionBase):
|
||||
for leave_type in leave_types:
|
||||
leave_type_map[leave_type.name] = leave_type
|
||||
|
||||
attendances = frappe.db.sql(
|
||||
attendances = frappe.db.sql( # nosemgrep
|
||||
"""
|
||||
SELECT attendance_date, status, leave_type
|
||||
FROM `tabAttendance`
|
||||
@@ -516,7 +533,7 @@ class SalarySlip(TransactionBase):
|
||||
AND docstatus = 1
|
||||
AND attendance_date between %s and %s
|
||||
""",
|
||||
values=(self.employee, self.start_date, self.end_date),
|
||||
values=(self.employee, self.start_date, end_date),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -267,7 +267,6 @@ class TestSalarySlip(FrappeTestCase):
|
||||
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
|
||||
|
||||
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
|
||||
leave_type_ppl.save()
|
||||
|
||||
alloc = create_leave_allocation(
|
||||
employee=emp_id,
|
||||
@@ -1128,6 +1127,35 @@ class TestSalarySlip(FrappeTestCase):
|
||||
if deduction.salary_component == "TDS":
|
||||
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
|
||||
|
||||
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||
def test_lwp_calculation_based_on_relieving_date(self):
|
||||
emp_id = make_employee("test_lwp_based_on_relieving_date@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||
|
||||
month_start_date = get_first_day(nowdate())
|
||||
first_sunday = get_first_sunday(for_date=month_start_date)
|
||||
relieving_date = add_days(first_sunday, 10)
|
||||
leave_start_date = add_days(first_sunday, 16)
|
||||
leave_end_date = add_days(leave_start_date, 2)
|
||||
|
||||
make_leave_application(emp_id, leave_start_date, leave_end_date, "Leave Without Pay")
|
||||
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": relieving_date, "status": "Left"})
|
||||
|
||||
ss = make_employee_salary_slip(
|
||||
"test_lwp_based_on_relieving_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Leave Application",
|
||||
)
|
||||
|
||||
holidays = ss.get_holidays_for_employee(month_start_date, relieving_date)
|
||||
days_between_start_and_relieving = date_diff(relieving_date, month_start_date) + 1
|
||||
|
||||
self.assertEqual(ss.leave_without_pay, 0)
|
||||
|
||||
self.assertEqual(ss.payment_days, (days_between_start_and_relieving - len(holidays)))
|
||||
|
||||
|
||||
def get_no_of_days():
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
@@ -1587,9 +1615,8 @@ def setup_test():
|
||||
frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None)
|
||||
|
||||
|
||||
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
||||
if not (from_date and to_date):
|
||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||
def make_holiday_list(list_name=None, from_date=None, to_date=None, add_weekly_offs=True):
|
||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||
name = list_name or "Salary Slip Test Holiday List"
|
||||
|
||||
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
||||
@@ -1600,10 +1627,13 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
||||
"holiday_list_name": name,
|
||||
"from_date": from_date or fiscal_year[1],
|
||||
"to_date": to_date or fiscal_year[2],
|
||||
"weekly_off": "Sunday",
|
||||
}
|
||||
).insert()
|
||||
holiday_list.get_weekly_off_dates()
|
||||
|
||||
if add_weekly_offs:
|
||||
holiday_list.weekly_off = "Sunday"
|
||||
holiday_list.get_weekly_off_dates()
|
||||
|
||||
holiday_list.save()
|
||||
holiday_list = holiday_list.name
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
}
|
||||
else {
|
||||
let qty = item.qty || 1;
|
||||
qty = me.frm.doc.is_return ? -1 * qty : qty;
|
||||
// allow for '0' qty on Credit/Debit notes
|
||||
let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
|
||||
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
||||
}
|
||||
|
||||
|
||||
@@ -1975,11 +1975,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
get_advances: function() {
|
||||
if(!this.frm.is_return) {
|
||||
var me = this;
|
||||
return this.frm.call({
|
||||
method: "set_advances",
|
||||
doc: this.frm.doc,
|
||||
callback: function(r, rt) {
|
||||
refresh_field("advances");
|
||||
me.frm.dirty();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1101,18 +1101,21 @@ def update_taxable_values(doc, method):
|
||||
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
else:
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
else:
|
||||
rate_of_depreciation = row.rate_of_depreciation
|
||||
# if its the first depreciation
|
||||
|
||||
@@ -123,7 +123,7 @@ frappe.ui.form.on("Customer", {
|
||||
|
||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||
frappe.set_route('query-report', 'General Ledger',
|
||||
{party_type: 'Customer', party: frm.doc.name});
|
||||
{party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name});
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(__('Pricing Rule'), function () {
|
||||
|
||||
@@ -275,18 +275,9 @@ class Customer(TransactionBase):
|
||||
|
||||
def on_trash(self):
|
||||
if self.customer_primary_contact:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabCustomer`
|
||||
SET
|
||||
customer_primary_contact=null,
|
||||
customer_primary_address=null,
|
||||
mobile_no=null,
|
||||
email_id=null,
|
||||
primary_address=null
|
||||
WHERE name=%(name)s""",
|
||||
{"name": self.name},
|
||||
)
|
||||
self.db_set("customer_primary_contact", None)
|
||||
if self.customer_primary_address:
|
||||
self.db_set("customer_primary_address", None)
|
||||
|
||||
delete_contact_and_address("Customer", self.name)
|
||||
if self.lead_name:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt
|
||||
@@ -343,6 +344,37 @@ class TestCustomer(FrappeTestCase):
|
||||
due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
|
||||
self.assertEqual(due_date, "2017-01-22")
|
||||
|
||||
def test_serach_fields_for_customer(self):
|
||||
from erpnext.controllers.queries import customer_query
|
||||
|
||||
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Naming Series")
|
||||
|
||||
make_property_setter(
|
||||
"Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype"
|
||||
)
|
||||
|
||||
data = customer_query(
|
||||
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, "_Test Customer")
|
||||
self.assertEqual(data[0].customer_group, "_Test Customer Group")
|
||||
self.assertTrue("territory" not in data[0])
|
||||
|
||||
make_property_setter(
|
||||
"Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype"
|
||||
)
|
||||
data = customer_query(
|
||||
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, "_Test Customer")
|
||||
self.assertEqual(data[0].customer_group, "_Test Customer Group")
|
||||
self.assertEqual(data[0].territory, "_Test Territory")
|
||||
self.assertTrue("territory" in data[0])
|
||||
|
||||
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name")
|
||||
|
||||
|
||||
def get_customer_dict(customer_name):
|
||||
return {
|
||||
|
||||
@@ -280,9 +280,12 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
|
||||
make_work_order() {
|
||||
var me = this;
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'get_work_order_items',
|
||||
me.frm.call({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
|
||||
args: {
|
||||
sales_order: this.frm.docname,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if(!r.message) {
|
||||
frappe.msgprint({
|
||||
@@ -292,14 +295,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if(!r.message) {
|
||||
frappe.msgprint({
|
||||
title: __('Work Order not created'),
|
||||
message: __('Work Order already created for all items with BOM'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
else {
|
||||
const fields = [{
|
||||
label: 'Items',
|
||||
fieldtype: 'Table',
|
||||
@@ -400,9 +396,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
make_raw_material_request: function() {
|
||||
var me = this;
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'get_work_order_items',
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
|
||||
args: {
|
||||
sales_order: this.frm.docname,
|
||||
for_raw_material_request: 1
|
||||
},
|
||||
callback: function(r) {
|
||||
@@ -421,6 +417,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
},
|
||||
|
||||
make_raw_material_request_dialog: function(r) {
|
||||
var me = this;
|
||||
var fields = [
|
||||
{fieldtype:'Check', fieldname:'include_exploded_items',
|
||||
label: __('Include Exploded Items')},
|
||||
|
||||
@@ -6,11 +6,12 @@ import json
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
|
||||
from six import string_types
|
||||
|
||||
@@ -21,6 +22,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_items_for_material_requests,
|
||||
)
|
||||
@@ -52,6 +56,7 @@ class SalesOrder(SellingController):
|
||||
self.validate_warehouse()
|
||||
self.validate_drop_ship()
|
||||
self.validate_serial_no_based_delivery()
|
||||
validate_against_blanket_order(self)
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.customer, self.company, self.inter_company_order_reference
|
||||
)
|
||||
@@ -481,51 +486,6 @@ class SalesOrder(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_work_order_items(self, for_raw_material_request=0):
|
||||
"""Returns items with BOM that already do not have a linked work order"""
|
||||
items = []
|
||||
item_codes = [i.item_code for i in self.items]
|
||||
product_bundle_parents = [
|
||||
pb.new_item_code
|
||||
for pb in frappe.get_all(
|
||||
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
|
||||
)
|
||||
]
|
||||
|
||||
for table in [self.items, self.packed_items]:
|
||||
for i in table:
|
||||
bom = get_default_bom(i.item_code)
|
||||
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
|
||||
|
||||
if not for_raw_material_request:
|
||||
total_work_order_qty = flt(
|
||||
frappe.db.sql(
|
||||
"""select sum(qty) from `tabWork Order`
|
||||
where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
|
||||
(i.item_code, self.name, i.name),
|
||||
)[0][0]
|
||||
)
|
||||
pending_qty = stock_qty - total_work_order_qty
|
||||
else:
|
||||
pending_qty = stock_qty
|
||||
|
||||
if pending_qty and i.item_code not in product_bundle_parents:
|
||||
items.append(
|
||||
dict(
|
||||
name=i.name,
|
||||
item_code=i.item_code,
|
||||
description=i.description,
|
||||
bom=bom or "",
|
||||
warehouse=i.warehouse,
|
||||
pending_qty=pending_qty,
|
||||
required_qty=pending_qty if for_raw_material_request else 0,
|
||||
sales_order_item=i.name,
|
||||
)
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
|
||||
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
|
||||
@@ -1399,3 +1359,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
|
||||
return
|
||||
|
||||
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_work_order_items(sales_order, for_raw_material_request=0):
|
||||
"""Returns items with BOM that already do not have a linked work order"""
|
||||
if sales_order:
|
||||
so = frappe.get_doc("Sales Order", sales_order)
|
||||
|
||||
wo = qb.DocType("Work Order")
|
||||
|
||||
items = []
|
||||
item_codes = [i.item_code for i in so.items]
|
||||
product_bundle_parents = [
|
||||
pb.new_item_code
|
||||
for pb in frappe.get_all(
|
||||
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
|
||||
)
|
||||
]
|
||||
|
||||
for table in [so.items, so.packed_items]:
|
||||
for i in table:
|
||||
bom = get_default_bom(i.item_code)
|
||||
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
|
||||
|
||||
if not for_raw_material_request:
|
||||
total_work_order_qty = flt(
|
||||
qb.from_(wo)
|
||||
.select(Sum(wo.qty))
|
||||
.where(
|
||||
(wo.production_item == i.item_code)
|
||||
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
|
||||
& (wo.docstatus.lte(2))
|
||||
)
|
||||
.run()[0][0]
|
||||
)
|
||||
pending_qty = stock_qty - total_work_order_qty
|
||||
else:
|
||||
pending_qty = stock_qty
|
||||
|
||||
if pending_qty and i.item_code not in product_bundle_parents:
|
||||
items.append(
|
||||
dict(
|
||||
name=i.name,
|
||||
item_code=i.item_code,
|
||||
description=i.description,
|
||||
bom=bom or "",
|
||||
warehouse=i.warehouse,
|
||||
pending_qty=pending_qty,
|
||||
required_qty=pending_qty if for_raw_material_request else 0,
|
||||
sales_order_item=i.name,
|
||||
)
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
@@ -1211,6 +1211,8 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertTrue(si.get("payment_schedule"))
|
||||
|
||||
def test_make_work_order(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
|
||||
|
||||
# Make a new Sales Order
|
||||
so = make_sales_order(
|
||||
**{
|
||||
@@ -1224,7 +1226,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
# Raise Work Orders
|
||||
po_items = []
|
||||
so_item_name = {}
|
||||
for item in so.get_work_order_items():
|
||||
for item in get_work_order_items(so.name):
|
||||
po_items.append(
|
||||
{
|
||||
"warehouse": item.get("warehouse"),
|
||||
@@ -1415,6 +1417,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
|
||||
|
||||
make_item( # template item
|
||||
"Test-WO-Tshirt",
|
||||
@@ -1454,7 +1457,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
]
|
||||
}
|
||||
)
|
||||
wo_items = so.get_work_order_items()
|
||||
wo_items = get_work_order_items(so.name)
|
||||
|
||||
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
|
||||
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
|
||||
@@ -1464,6 +1467,8 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
|
||||
|
||||
def test_request_for_raw_materials(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
|
||||
|
||||
item = make_item(
|
||||
"_Test Finished Item",
|
||||
{
|
||||
@@ -1496,7 +1501,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
|
||||
so.submit()
|
||||
mr_dict = frappe._dict()
|
||||
items = so.get_work_order_items(1)
|
||||
items = get_work_order_items(so.name, 1)
|
||||
mr_dict["items"] = items
|
||||
mr_dict["include_exploded_items"] = 0
|
||||
mr_dict["ignore_existing_ordered_qty"] = 1
|
||||
|
||||
@@ -30,12 +30,18 @@
|
||||
"so_required",
|
||||
"dn_required",
|
||||
"sales_update_frequency",
|
||||
"over_order_allowance",
|
||||
"allow_multiple_items",
|
||||
"allow_against_multiple_purchase_orders",
|
||||
"hide_tax_id",
|
||||
"allow_sales_order_creation_for_expired_quotation"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "customer_defaults_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Customer Defaults"
|
||||
},
|
||||
{
|
||||
"default": "Customer Name",
|
||||
"fieldname": "cust_master_name",
|
||||
@@ -44,13 +50,6 @@
|
||||
"label": "Customer Naming By",
|
||||
"options": "Customer Name\nNaming Series\nAuto Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign_naming_by",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Campaign Naming By",
|
||||
"options": "Campaign Name\nNaming Series\nAuto Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
@@ -58,6 +57,10 @@
|
||||
"label": "Default Customer Group",
|
||||
"options": "Customer Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
@@ -66,11 +69,31 @@
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"fieldname": "selling_price_list",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "crm_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "CRM Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign_naming_by",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Default Price List",
|
||||
"options": "Price List"
|
||||
"label": "Campaign Naming By",
|
||||
"options": "Campaign Name\nNaming Series\nAuto Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "contract_naming_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Contract Naming By",
|
||||
"options": "Party Name\nNaming Series"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_valid_till",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Quotation Validity Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "15",
|
||||
@@ -80,9 +103,65 @@
|
||||
"label": "Close Opportunity After Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_valid_till",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Quotation Validity Days"
|
||||
"fieldname": "item_price_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Price Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "selling_price_list",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Default Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_sales_rate",
|
||||
"fieldname": "maintain_same_rate_action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
|
||||
"mandatory_depends_on": "maintain_same_sales_rate",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||
"fieldname": "role_to_override_stop_action",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_sales_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout Sales Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "editable_price_list_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow User to Edit Price List Rate in Transactions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "validate_selling_price",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "editable_bundle_item_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate Product Bundle Price based on Child Items' Rates"
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "so_required",
|
||||
@@ -107,15 +186,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_sales_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout Sales Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "editable_price_list_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow User to Edit Price List Rate in Transactions"
|
||||
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -129,83 +203,17 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Multiple Sales Orders Against a Customer's Purchase Order"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "validate_selling_price",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hide_tax_id",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Customer's Tax ID from Sales Transactions"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_sales_rate",
|
||||
"fieldname": "maintain_same_rate_action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
|
||||
"mandatory_depends_on": "maintain_same_sales_rate",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||
"fieldname": "role_to_override_stop_action",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "editable_bundle_item_rates",
|
||||
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate Product Bundle Price based on Child Items' Rates"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_defaults_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Customer Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "crm_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "CRM Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_price_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Price Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "contract_naming_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Contract Naming By",
|
||||
"options": "Party Name\nNaming Series"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Sales Order Creation For Expired Quotation"
|
||||
"label": "Allow Sales Order Creation For Expired Quotation"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -213,7 +221,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-04 12:37:53.380857",
|
||||
"modified": "2023-03-22 13:09:38.513317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
||||
@@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
const from_selector = field === 'qty' && value === "+1";
|
||||
if (from_selector)
|
||||
value = flt(item_row.qty) + flt(value);
|
||||
value = flt(item_row.stock_qty) + flt(value);
|
||||
|
||||
if (item_row_exists) {
|
||||
if (field === 'qty')
|
||||
|
||||
@@ -417,9 +417,14 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
|
||||
} else {
|
||||
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
|
||||
if (r.message.batch_no != null) {
|
||||
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message.batch_no);
|
||||
} else if (r.message.msg_print) {
|
||||
frappe.show_alert({
|
||||
message: r.message.msg_print,
|
||||
indicator:'orange'
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ def add_standard_navbar_items():
|
||||
{
|
||||
"item_label": "Documentation",
|
||||
"item_type": "Route",
|
||||
"route": "https://erpnext.com/docs/user/manual",
|
||||
"route": "https://docs.erpnext.com/docs/v13/user/manual/en/introduction",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -260,7 +260,9 @@ def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
|
||||
warehouse = d.get(warehouse_field, None)
|
||||
if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"):
|
||||
if not d.batch_no:
|
||||
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
|
||||
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no).get(
|
||||
"batch_no", None
|
||||
)
|
||||
else:
|
||||
batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse)
|
||||
if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")):
|
||||
@@ -282,6 +284,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
"""
|
||||
|
||||
batch_no = None
|
||||
message = None
|
||||
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
|
||||
|
||||
for batch in batches:
|
||||
@@ -290,15 +293,18 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
break
|
||||
|
||||
if not batch_no:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
|
||||
).format(frappe.bold(item_code))
|
||||
)
|
||||
message = _(
|
||||
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
|
||||
).format(frappe.bold(item_code))
|
||||
if throw:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
|
||||
).format(frappe.bold(item_code))
|
||||
)
|
||||
raise UnableToSelectBatchError
|
||||
|
||||
return batch_no
|
||||
return {"batch_no": batch_no, "msg_print": message}
|
||||
|
||||
|
||||
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
|
||||
@@ -99,7 +99,8 @@ class TestBatch(FrappeTestCase):
|
||||
|
||||
# shipped from FEFO batch
|
||||
self.assertEqual(
|
||||
delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
|
||||
delivery_note.items[0].batch_no,
|
||||
get_batch_no(item_code, receipt.items[0].warehouse, batch_qty).get("batch_no", None),
|
||||
)
|
||||
|
||||
def test_delivery_note_fail(self):
|
||||
@@ -145,7 +146,8 @@ class TestBatch(FrappeTestCase):
|
||||
|
||||
# assert same batch is selected
|
||||
self.assertEqual(
|
||||
stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
|
||||
stock_entry.items[0].batch_no,
|
||||
get_batch_no(item_code, receipt.items[0].warehouse, batch_qty).get("batch_no", None),
|
||||
)
|
||||
|
||||
def test_batch_split(self):
|
||||
|
||||
@@ -33,6 +33,9 @@ frappe.ui.form.on("Item", {
|
||||
'Material Request': () => {
|
||||
open_form(frm, "Material Request", "Material Request Item", "items");
|
||||
},
|
||||
'Stock Entry': () => {
|
||||
open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
|
||||
},
|
||||
};
|
||||
|
||||
},
|
||||
@@ -848,6 +851,9 @@ function open_form(frm, doctype, child_doctype, parentfield) {
|
||||
new_child_doc.item_name = frm.doc.item_name;
|
||||
new_child_doc.uom = frm.doc.stock_uom;
|
||||
new_child_doc.description = frm.doc.description;
|
||||
if (!new_child_doc.qty) {
|
||||
new_child_doc.qty = 1.0;
|
||||
}
|
||||
|
||||
frappe.run_serially([
|
||||
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),
|
||||
|
||||
@@ -54,7 +54,7 @@ class ItemAlternative(Document):
|
||||
if not item_data.allow_alternative_item:
|
||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||
if self.two_way and not alternative_item_data.allow_alternative_item:
|
||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||
frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
|
||||
|
||||
def validate_duplicate(self):
|
||||
if frappe.db.get_value(
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on("Item Price", {
|
||||
onload: function (frm) {
|
||||
setup(frm) {
|
||||
frm.set_query("item_code", function() {
|
||||
return {
|
||||
filters: {
|
||||
"disabled": 0,
|
||||
"has_variants": 0
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload(frm) {
|
||||
// Fetch price list details
|
||||
frm.add_fetch("price_list", "buying", "buying");
|
||||
frm.add_fetch("price_list", "selling", "selling");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate
|
||||
|
||||
@@ -19,6 +19,7 @@ class ItemPrice(Document):
|
||||
self.update_price_list_details()
|
||||
self.update_item_details()
|
||||
self.check_duplicates()
|
||||
self.validate_item_template()
|
||||
|
||||
def validate_item(self):
|
||||
if not frappe.db.exists("Item", self.item_code):
|
||||
@@ -47,6 +48,12 @@ class ItemPrice(Document):
|
||||
"Item", self.item_code, ["item_name", "description"]
|
||||
)
|
||||
|
||||
def validate_item_template(self):
|
||||
if frappe.get_cached_value("Item", self.item_code, "has_variants"):
|
||||
msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def check_duplicates(self):
|
||||
conditions = (
|
||||
"""where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
|
||||
|
||||
@@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
|
||||
frappe.db.sql("delete from `tabItem Price`")
|
||||
make_test_records_for_doctype("Item Price", force=True)
|
||||
|
||||
def test_template_item_price(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(
|
||||
"Test Template Item 1",
|
||||
{
|
||||
"has_variants": 1,
|
||||
"variant_based_on": "Manufacturer",
|
||||
},
|
||||
)
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"price_list": "_Test Price List",
|
||||
"item_code": item.name,
|
||||
"price_list_rate": 100,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_duplicate_item(self):
|
||||
doc = frappe.copy_doc(test_records[0])
|
||||
self.assertRaises(ItemPriceDuplicateItem, doc.save)
|
||||
|
||||
@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
|
||||
self.get_items_from_purchase_receipts()
|
||||
|
||||
self.set_applicable_charges_on_item()
|
||||
self.validate_applicable_charges_for_item()
|
||||
|
||||
def check_mandatory(self):
|
||||
if not self.get("purchase_receipts"):
|
||||
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
|
||||
total_item_cost += item.get(based_on_field)
|
||||
|
||||
for item in self.get("items"):
|
||||
if not total_item_cost and not item.get(based_on_field):
|
||||
frappe.throw(
|
||||
_(
|
||||
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
|
||||
)
|
||||
)
|
||||
|
||||
item.applicable_charges = flt(
|
||||
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
|
||||
item.precision("applicable_charges"),
|
||||
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_applicable_charges_for_item()
|
||||
self.update_landed_cost()
|
||||
|
||||
def on_cancel(self):
|
||||
|
||||
@@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||
|
||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||
"Test impact of LCV on future stock balances."
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item("LCV Stock Item", {"is_stock_item": 1})
|
||||
warehouse = "Stores - _TC"
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
warehouse=warehouse,
|
||||
qty=10,
|
||||
rate=0,
|
||||
posting_date=add_days(frappe.utils.nowdate(), -2),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
"stock_value_difference",
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=100,
|
||||
distribute_charges_based_on="Distribute Manually",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.items[0].applicable_charges = 100
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
"stock_value_difference",
|
||||
),
|
||||
100,
|
||||
)
|
||||
|
||||
def test_landed_cost_voucher_against_purchase_invoice(self):
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
|
||||
|
||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||
lcv.company = args.company or "_Test Company"
|
||||
lcv.distribute_charges_based_on = "Amount"
|
||||
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
|
||||
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||
from six import string_types
|
||||
|
||||
@@ -185,6 +186,34 @@ class MaterialRequest(BuyingController):
|
||||
self.update_requested_qty()
|
||||
self.update_requested_qty_in_production_plan()
|
||||
|
||||
def get_mr_items_ordered_qty(self, mr_items):
|
||||
mr_items_ordered_qty = {}
|
||||
mr_items = [d.name for d in self.get("items") if d.name in mr_items]
|
||||
|
||||
doctype = qty_field = None
|
||||
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
||||
doctype = frappe.qb.DocType("Stock Entry Detail")
|
||||
qty_field = doctype.transfer_qty
|
||||
elif self.material_request_type == "Manufacture":
|
||||
doctype = frappe.qb.DocType("Work Order")
|
||||
qty_field = doctype.qty
|
||||
|
||||
if doctype and qty_field:
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(doctype.material_request_item, Sum(qty_field))
|
||||
.where(
|
||||
(doctype.material_request == self.name)
|
||||
& (doctype.material_request_item.isin(mr_items))
|
||||
& (doctype.docstatus == 1)
|
||||
)
|
||||
.groupby(doctype.material_request_item)
|
||||
)
|
||||
|
||||
mr_items_ordered_qty = frappe._dict(query.run())
|
||||
|
||||
return mr_items_ordered_qty
|
||||
|
||||
def update_completed_qty(self, mr_items=None, update_modified=True):
|
||||
if self.material_request_type == "Purchase":
|
||||
return
|
||||
@@ -192,18 +221,13 @@ class MaterialRequest(BuyingController):
|
||||
if not mr_items:
|
||||
mr_items = [d.name for d in self.get("items")]
|
||||
|
||||
mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
|
||||
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.name in mr_items:
|
||||
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
||||
d.ordered_qty = flt(
|
||||
frappe.db.sql(
|
||||
"""select sum(transfer_qty)
|
||||
from `tabStock Entry Detail` where material_request = %s
|
||||
and material_request_item = %s and docstatus = 1""",
|
||||
(self.name, d.name),
|
||||
)[0][0]
|
||||
)
|
||||
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
|
||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||
|
||||
if mr_qty_allowance:
|
||||
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
|
||||
@@ -224,14 +248,7 @@ class MaterialRequest(BuyingController):
|
||||
)
|
||||
|
||||
elif self.material_request_type == "Manufacture":
|
||||
d.ordered_qty = flt(
|
||||
frappe.db.sql(
|
||||
"""select sum(qty)
|
||||
from `tabWork Order` where material_request = %s
|
||||
and material_request_item = %s and docstatus = 1""",
|
||||
(self.name, d.name),
|
||||
)[0][0]
|
||||
)
|
||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||
|
||||
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
|
||||
|
||||
@@ -594,6 +611,9 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.purpose = source.material_request_type
|
||||
target.from_warehouse = source.set_from_warehouse
|
||||
target.to_warehouse = source.set_warehouse
|
||||
|
||||
if source.job_card:
|
||||
target.purpose = "Material Transfer for Manufacture"
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
divisional_loss = flt(
|
||||
valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
|
||||
valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
|
||||
)
|
||||
|
||||
if divisional_loss:
|
||||
@@ -831,7 +831,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
|
||||
# Update Billing % based on pending accepted qty
|
||||
total_amount, total_billed_amount = 0, 0
|
||||
for item in pr_doc.items:
|
||||
return_data = frappe.db.get_list(
|
||||
return_data = frappe.get_all(
|
||||
"Purchase Receipt",
|
||||
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
|
||||
filters=[
|
||||
@@ -1064,13 +1064,25 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
|
||||
)
|
||||
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
|
||||
"amount"
|
||||
] += (account.amount * item.get(based_on_field) / total_item_cost)
|
||||
if total_item_cost > 0:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["amount"] += (
|
||||
account.amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
|
||||
"base_amount"
|
||||
] += (account.base_amount * item.get(based_on_field) / total_item_cost)
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["base_amount"] += (
|
||||
account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
else:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["amount"] += item.applicable_charges
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["base_amount"] += item.applicable_charges
|
||||
|
||||
return item_account_wise_cost
|
||||
|
||||
|
||||
@@ -1262,7 +1262,9 @@ class StockEntry(StockController):
|
||||
and ret.get("has_batch_no")
|
||||
and not args.get("batch_no")
|
||||
):
|
||||
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
|
||||
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]).get(
|
||||
"batch_no", None
|
||||
)
|
||||
|
||||
if (
|
||||
self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code")
|
||||
@@ -2227,11 +2229,11 @@ class StockEntry(StockController):
|
||||
|
||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||
|
||||
def set_missing_values(self):
|
||||
def set_missing_values(self, raise_error_if_no_rate=True):
|
||||
"Updates rate and availability of all the items of mapped doc."
|
||||
self.set_transfer_qty()
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
self.calculate_rate_and_amount(raise_error_if_no_rate=raise_error_if_no_rate)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -2,7 +2,22 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Stock Reposting Settings', {
|
||||
// refresh: function(frm) {
|
||||
refresh: function(frm) {
|
||||
frm.trigger('convert_to_item_based_reposting');
|
||||
},
|
||||
|
||||
// }
|
||||
convert_to_item_based_reposting: function(frm) {
|
||||
frm.add_custom_button(__('Convert to Item Based Reposting'), function() {
|
||||
frm.call({
|
||||
method: 'convert_to_item_wh_reposting',
|
||||
frezz: true,
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
|
||||
|
||||
@@ -24,3 +26,62 @@ class StockRepostingSettings(Document):
|
||||
|
||||
if diff < 10:
|
||||
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_to_item_wh_reposting(self):
|
||||
"""Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
|
||||
|
||||
reposting_data = get_reposting_entries()
|
||||
|
||||
vouchers = [d.voucher_no for d in reposting_data]
|
||||
|
||||
item_warehouses = {}
|
||||
|
||||
for ledger in get_stock_ledgers(vouchers):
|
||||
key = (ledger.item_code, ledger.warehouse)
|
||||
if key not in item_warehouses:
|
||||
item_warehouses[key] = ledger.posting_date
|
||||
elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date):
|
||||
item_warehouses[key] = ledger.posting_date
|
||||
|
||||
for key, posting_date in item_warehouses.items():
|
||||
item_code, warehouse = key
|
||||
create_repost_item_valuation(item_code, warehouse, posting_date)
|
||||
|
||||
for row in reposting_data:
|
||||
frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped")
|
||||
|
||||
self.db_set("item_based_reposting", 1)
|
||||
frappe.msgprint(_("Item Warehouse based reposting has been enabled."))
|
||||
|
||||
|
||||
def get_reposting_entries():
|
||||
return frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
fields=["voucher_no", "name"],
|
||||
filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"},
|
||||
)
|
||||
|
||||
|
||||
def get_stock_ledgers(vouchers):
|
||||
return frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["item_code", "warehouse", "posting_date"],
|
||||
filters={"voucher_no": ("in", vouchers)},
|
||||
)
|
||||
|
||||
|
||||
def create_repost_item_valuation(item_code, warehouse, posting_date):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
|
||||
"posting_date": posting_date,
|
||||
"based_on": "Item and Warehouse",
|
||||
"posting_time": "00:00:01",
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"allow_negative_stock": True,
|
||||
"status": "Queued",
|
||||
}
|
||||
).submit()
|
||||
|
||||
@@ -153,7 +153,7 @@ def update_stock(args, out):
|
||||
):
|
||||
|
||||
if out.has_batch_no and not args.get("batch_no"):
|
||||
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
|
||||
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty).get("batch_no", None)
|
||||
actual_batch_qty = get_batch_qty(out.batch_no, out.warehouse, out.item_code)
|
||||
if actual_batch_qty:
|
||||
out.update(actual_batch_qty)
|
||||
|
||||
@@ -6,6 +6,7 @@ from operator import itemgetter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cint, date_diff, flt, getdate
|
||||
from six import iteritems
|
||||
|
||||
@@ -276,11 +277,39 @@ def get_stock_ledger_entries(filters, items):
|
||||
)
|
||||
|
||||
|
||||
def get_opening_vouchers(to_date):
|
||||
opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []}
|
||||
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
sr = frappe.qb.DocType("Stock Reconciliation")
|
||||
|
||||
vouchers_data = (
|
||||
frappe.qb.from_(
|
||||
(
|
||||
frappe.qb.from_(se)
|
||||
.select(se.name, Coalesce("Stock Entry").as_("voucher_type"))
|
||||
.where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes"))
|
||||
)
|
||||
+ (
|
||||
frappe.qb.from_(sr)
|
||||
.select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type"))
|
||||
.where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock"))
|
||||
)
|
||||
).select("voucher_type", "name")
|
||||
).run(as_dict=True)
|
||||
|
||||
if vouchers_data:
|
||||
for d in vouchers_data:
|
||||
opening_vouchers[d.voucher_type].append(d.name)
|
||||
|
||||
return opening_vouchers
|
||||
|
||||
|
||||
def get_item_warehouse_map(filters, sle):
|
||||
iwb_map = {}
|
||||
from_date = getdate(filters.get("from_date"))
|
||||
to_date = getdate(filters.get("to_date"))
|
||||
|
||||
opening_vouchers = get_opening_vouchers(to_date)
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||
|
||||
for d in sle:
|
||||
@@ -309,11 +338,7 @@ def get_item_warehouse_map(filters, sle):
|
||||
|
||||
value_diff = flt(d.stock_value_difference)
|
||||
|
||||
if d.posting_date < from_date or (
|
||||
d.posting_date == from_date
|
||||
and d.voucher_type == "Stock Reconciliation"
|
||||
and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"
|
||||
):
|
||||
if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []):
|
||||
qty_dict.opening_qty += qty_diff
|
||||
qty_dict.opening_val += value_diff
|
||||
|
||||
|
||||
@@ -2007,30 +2007,27 @@ Please identify/create Account (Ledger) for type - {0},Bitte identifizieren / er
|
||||
Please login as another user to register on Marketplace,"Bitte melden Sie sich als anderer Benutzer an, um sich auf dem Marktplatz zu registrieren",
|
||||
Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,"Bitte sicher stellen, dass wirklich alle Transaktionen dieses Unternehmens gelöscht werden sollen. Die Stammdaten bleiben bestehen. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
Please mention Basic and HRA component in Company,Bitte erwähnen Sie die Basis- und HRA-Komponente in der Firma,
|
||||
Please mention Round Off Account in Company,Bitte Abschlusskonto in Unternehmen vermerken,
|
||||
Please mention Round Off Cost Center in Company,Bitte Abschlusskostenstelle in Unternehmen vermerken,
|
||||
Please mention no of visits required,"Bitte bei ""Besuche erforderlich"" NEIN angeben",
|
||||
Please mention the Lead Name in Lead {0},Bitte erwähnen Sie den Lead Name in Lead {0},
|
||||
Please pull items from Delivery Note,Bitte Artikel vom Lieferschein nehmen,
|
||||
Please mention Round Off Account in Company,Bitte ein Standardkonto Konto für Rundungsdifferenzen in Unternehmen einstellen,
|
||||
Please mention Round Off Cost Center in Company,Bitte eine Kostenstelle für Rundungsdifferenzen in Unternehmen einstellen,
|
||||
Please mention no of visits required,Bitte die Anzahl der benötigten Wartungsbesuche angeben,
|
||||
Please pull items from Delivery Note,Bitte Artikel aus dem Lieferschein ziehen,
|
||||
Please register the SIREN number in the company information file,Bitte registrieren Sie die SIREN-Nummer in der Unternehmensinformationsdatei,
|
||||
Please remove this Invoice {0} from C-Form {1},Bitte diese Rechnung {0} vom Kontaktformular {1} entfernen,
|
||||
Please save the patient first,Bitte speichern Sie den Patienten zuerst,
|
||||
Please save the report again to rebuild or update,"Speichern Sie den Bericht erneut, um ihn neu zu erstellen oder zu aktualisieren",
|
||||
"Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Bitte zugewiesenen Betrag, Rechnungsart und Rechnungsnummer in mindestens einer Zeile auswählen",
|
||||
Please select Apply Discount On,"Bitte ""Rabatt anwenden auf"" auswählen",
|
||||
Please select BOM against item {0},Bitte wählen Sie Stückliste gegen Artikel {0},
|
||||
Please select BOM for Item in Row {0},Bitte Stückliste für Artikel in Zeile {0} auswählen,
|
||||
Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
|
||||
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 BOM against item {0},Bitte eine Stückliste für Artikel {0} auswählen,
|
||||
Please select BOM for Item in Row {0},Bitte eine Stückliste für den Artikel in Zeile {0} auswählen,
|
||||
Please select BOM in BOM field for Item {0},Bitte im Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
|
||||
Please select Category first,Bitte zuerst eine Kategorie auswählen,
|
||||
Please select Charge Type first,Bitte zuerst einen Chargentyp auswählen,
|
||||
Please select Company,Bitte ein Unternehmen auswählen,
|
||||
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,
|
||||
Please select Completion Date for Completed Repair,Bitte wählen Sie das Abschlussdatum für die abgeschlossene Reparatur,
|
||||
Please select Course,Bitte wählen Sie Kurs,
|
||||
Please select Drug,Bitte wählen Sie Arzneimittel,
|
||||
Please select Employee,Bitte wählen Sie Mitarbeiter,
|
||||
Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie Bestehende Unternehmen für die Erstellung von Konten,
|
||||
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
|
||||
@@ -4047,7 +4044,7 @@ Server Error,Serverfehler,
|
||||
Service Level Agreement has been changed to {0}.,Service Level Agreement wurde in {0} geändert.,
|
||||
Service Level Agreement was reset.,Service Level Agreement wurde zurückgesetzt.,
|
||||
Service Level Agreement with Entity Type {0} and Entity {1} already exists.,Service Level Agreement mit Entitätstyp {0} und Entität {1} ist bereits vorhanden.,
|
||||
Set,Menge,
|
||||
Set Loyalty Program,Treueprogramm eintragen,
|
||||
Set Meta Tags,Festlegen von Meta-Tags,
|
||||
Set {0} in company {1},{0} in Firma {1} festlegen,
|
||||
Setup,Einstellungen,
|
||||
@@ -4227,10 +4224,8 @@ To date cannot be before From date,Bis-Datum kann nicht vor Von-Datum liegen,
|
||||
Write Off,Abschreiben,
|
||||
{0} Created,{0} Erstellt,
|
||||
Email Id,E-Mail-ID,
|
||||
No,Kein,
|
||||
Reference Doctype,Referenz-DocType,
|
||||
User Id,Benutzeridentifikation,
|
||||
Yes,Ja,
|
||||
Actual ,Tatsächlich,
|
||||
Add to cart,In den Warenkorb legen,
|
||||
Budget,Budget,
|
||||
@@ -7799,7 +7794,7 @@ Default Employee Advance Account,Standardkonto für Vorschüsse an Arbeitnehmer,
|
||||
Default Cost of Goods Sold Account,Standard-Herstellkosten,
|
||||
Default Income Account,Standard-Ertragskonto,
|
||||
Default Deferred Revenue Account,Standardkonto für passive Rechnungsabgrenzung,
|
||||
Default Deferred Expense Account,Standard-Rechnungsabgrenzungsposten,
|
||||
Default Deferred Expense Account,Standardkonto für aktive Rechnungsabgrenzung,
|
||||
Default Payroll Payable Account,Standardkonto für Verbindlichkeiten aus Lohn und Gehalt,
|
||||
Default Expense Claim Payable Account,Standard-Expense Claim Zahlbares Konto,
|
||||
Stock Settings,Lager-Einstellungen,
|
||||
@@ -8867,7 +8862,7 @@ Add Topic to Courses,Hinzufügen eines Themas zu Kursen,
|
||||
This topic is already added to the existing courses,Dieses Thema wurde bereits zu den bestehenden Kursen hinzugefügt,
|
||||
"If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order","Wenn Shopify keinen Kunden in der Bestellung hat, berücksichtigt das System beim Synchronisieren der Bestellungen den Standardkunden für die Bestellung",
|
||||
The accounts are set by the system automatically but do confirm these defaults,"Die Konten werden vom System automatisch festgelegt, bestätigen jedoch diese Standardeinstellungen",
|
||||
Default Round Off Account,Standard-Rundungskonto,
|
||||
Default Round Off Account,Standardkonto für Rundungsdifferenzen,
|
||||
Failed Import Log,Importprotokoll fehlgeschlagen,
|
||||
Fixed Error Log,Fehlerprotokoll behoben,
|
||||
Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts,Firma {0} existiert bereits. Durch Fortfahren werden das Unternehmen und der Kontenplan überschrieben,
|
||||
@@ -9899,3 +9894,5 @@ Total Asset,Aktiva,
|
||||
Total Liability,Verbindlichkeiten,
|
||||
Total Equity,Eigenkapital,
|
||||
Warehouse wise Stock Value,Warenwert nach Lager,
|
||||
Discount Validity,Frist für den Rabatt,
|
||||
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
|
||||
Stock Levels,Niveaux du Stocks,
|
||||
Stock Liabilities,Passif du Stock,
|
||||
Stock Options,Options du Stock,
|
||||
Stock Qty,Qté en Stock,
|
||||
Stock Qty,Qté en unité de stock,
|
||||
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
|
||||
Stock Reports,Rapports de stock,
|
||||
Stock Summary,Résumé du Stock,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user