Merge branch 'develop' into subcontracting

This commit is contained in:
Sagar Sharma 2022-07-05 11:25:08 +05:30 committed by GitHub
commit f75d35ed60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 372 additions and 127 deletions

View File

@ -66,6 +66,7 @@ ignore =
F841, F841,
E713, E713,
E712, E712,
B023
max-line-length = 200 max-line-length = 200

View File

@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment' - name: 'Setup Environment'
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: '3.10'
- name: 'Clone repo' - name: 'Clone repo'
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -35,9 +35,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: "gabrielfalcao/pyenv-action@v9"
with: with:
python-version: 3.8 versions: 3.10:latest, 3.7:latest
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -52,7 +52,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-
@ -82,7 +82,10 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- name: Install - name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: |
pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
TYPE: server TYPE: server
@ -96,18 +99,23 @@ jobs:
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13) for version in $(seq 12 13)
do do
echo "Updating to v$version" echo "Updating to v$version"
branch_name="version-$version-hotfix" branch_name="version-$version-hotfix"
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/frappe" checkout -q -f $branch_name git -C "apps/frappe" checkout -q -f $branch_name
git -C "apps/erpnext" checkout -q -f $branch_name git -C "apps/erpnext" checkout -q -f $branch_name
bench setup requirements --python rm -rf ~/frappe-bench/env
bench setup env
bench pip install -e ./apps/erpnext
bench --site test_site migrate bench --site test_site migrate
done done
@ -115,5 +123,10 @@ jobs:
echo "Updating to latest version" echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
bench setup requirements --python
pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env
bench -v setup env
bench pip install -e ./apps/erpnext
bench --site test_site migrate bench --site test_site migrate

View File

@ -59,7 +59,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: '3.10'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -74,7 +74,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-

View File

@ -46,7 +46,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: '3.10'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -61,7 +61,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-

View File

@ -3,33 +3,30 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/erpnext_integrations/ @nextchamp-saqib
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007 erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib pos* @nextchamp-saqib
erpnext/buying/ @marination @rohitwaghchaure @ankush erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
erpnext/e_commerce/ @marination erpnext/e_commerce/ @marination
erpnext/maintenance/ @marination @rohitwaghchaure erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @marination @rohitwaghchaure @ankush erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
erpnext/portal/ @marination erpnext/portal/ @marination
erpnext/quality_management/ @marination @rohitwaghchaure erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
erpnext/shopping_cart/ @marination erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
erpnext/crm/ @ruchamahabal @pateljannat erpnext/crm/ @NagariaHussain
erpnext/education/ @ruchamahabal @pateljannat erpnext/education/ @rutwikhdev
erpnext/hr/ @ruchamahabal @pateljannat erpnext/projects/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination
erpnext/public/ @nextchamp-saqib @marination erpnext/public/ @nextchamp-saqib @marination
.github/ @ankush .github/ @ankush
requirements.txt @gavindsouza pyproject.toml @gavindsouza @ankush

View File

@ -1 +0,0 @@
hypothesis~=6.31.0

View File

@ -162,7 +162,7 @@ class PaymentReconciliation(Document):
{ {
"reference_type": inv.voucher_type, "reference_type": inv.voucher_type,
"reference_name": inv.voucher_no, "reference_name": inv.voucher_no,
"amount": -(inv.outstanding), "amount": -(inv.outstanding_in_account_currency),
"posting_date": inv.posting_date, "posting_date": inv.posting_date,
"currency": inv.currency, "currency": inv.currency,
} }

View File

@ -19,6 +19,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_company() self.create_company()
self.create_item() self.create_item()
self.create_customer() self.create_customer()
self.create_account()
self.clear_old_entries() self.clear_old_entries()
def tearDown(self): def tearDown(self):
@ -89,6 +90,38 @@ class TestPaymentReconciliation(FrappeTestCase):
customer.save() customer.save()
self.customer2 = customer.name self.customer2 = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 3"):
self.customer3 = "_Test PR Customer 3"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 3"
customer.type = "Individual"
customer.default_currency = "EUR"
customer.save()
self.customer3 = customer.name
def create_account(self):
account_name = "Debtors EUR"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - _PR"
acc.company = self.company
acc.account_currency = "EUR"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_eur = acc.name
def create_sales_invoice( def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
): ):
@ -454,3 +487,56 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("payments")), 1) self.assertEqual(len(pr.get("payments")), 1)
self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20) self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
self.assertEqual(pr.get("payments")[0].amount, 20) self.assertEqual(pr.get("payments")[0].amount, 20)
def test_pr_output_foreign_currency_and_amount(self):
# test for currency and amount invoices and payments
transaction_date = nowdate()
# In EUR
amount = 100
exchange_rate = 80
si = self.create_sales_invoice(
qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
si.customer = self.customer3
si.currency = "EUR"
si.conversion_rate = exchange_rate
si.debit_to = self.debtors_eur
si = si.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.customer = self.customer3
cr_note.is_return = 1
cr_note.currency = "EUR"
cr_note.conversion_rate = exchange_rate
cr_note.debit_to = self.debtors_eur
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer3
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.invoices[0].amount, amount)
self.assertEqual(pr.invoices[0].currency, "EUR")
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
cr_note.cancel()
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
pay = get_payment_entry(si.doctype, si.name)
pay.references.clear()
pay = pay.save().submit()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")

View File

@ -476,6 +476,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.trigger("calculate_timesheet_totals"); this.frm.trigger("calculate_timesheet_totals");
} }
} }
is_cash_or_non_trade_discount() {
this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount);
if (!this.frm.doc.is_cash_or_non_trade_discount) {
this.frm.set_value("additional_discount_account", "");
}
}
}; };
// for backward compatibility: combine new and previous states // for backward compatibility: combine new and previous states

View File

@ -106,6 +106,7 @@
"loyalty_redemption_cost_center", "loyalty_redemption_cost_center",
"section_break_49", "section_break_49",
"apply_discount_on", "apply_discount_on",
"is_cash_or_non_trade_discount",
"base_discount_amount", "base_discount_amount",
"additional_discount_account", "additional_discount_account",
"column_break_51", "column_break_51",
@ -1790,8 +1791,6 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate", "fieldname": "commission_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hide_days": 1, "hide_days": 1,
@ -1990,7 +1989,7 @@
{ {
"fieldname": "additional_discount_account", "fieldname": "additional_discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Additional Discount Account", "label": "Discount Account",
"options": "Account" "options": "Account"
}, },
{ {
@ -2028,6 +2027,13 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount Eligible for Commission", "label": "Amount Eligible for Commission",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
"fieldname": "is_cash_or_non_trade_discount",
"fieldtype": "Check",
"label": "Is Cash or Non Trade Discount"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2040,7 +2046,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-06-10 03:52:51.409913", "modified": "2022-06-16 16:22:44.870575",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1030,7 +1030,7 @@ class SalesInvoice(SellingController):
) )
if grand_total and not self.is_internal_transfer(): if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle # Did not use base_grand_total to book rounding loss gle
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
@ -1055,6 +1055,22 @@ class SalesInvoice(SellingController):
) )
) )
if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"):
gl_entries.append(
self.get_gl_dict(
{
"account": self.additional_discount_account,
"against": self.debit_to,
"debit": self.base_discount_amount,
"debit_in_account_currency": self.discount_amount,
"cost_center": self.cost_center,
"project": self.project,
},
self.currency,
item=self,
)
)
def make_tax_gl_entries(self, gl_entries): def make_tax_gl_entries(self, gl_entries):
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
@ -2117,6 +2133,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
source_document_warehouse_field = "from_warehouse" source_document_warehouse_field = "from_warehouse"
target_document_warehouse_field = "target_warehouse" target_document_warehouse_field = "target_warehouse"
received_items = get_received_items(source_name, target_doctype, target_detail_field)
validate_inter_company_transaction(source_doc, doctype) validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype) details = get_inter_company_details(source_doc, doctype)
@ -2181,12 +2199,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
shipping_address_name=target_doc.shipping_address_name, shipping_address_name=target_doc.shipping_address_name,
) )
def update_item(source, target, source_parent):
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
item_field_map = { item_field_map = {
"doctype": target_doctype + " Item", "doctype": target_doctype + " Item",
"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],
"field_map": { "field_map": {
"rate": "rate", "rate": "rate",
}, },
"postprocess": update_item,
"condition": lambda doc: doc.qty > 0,
} }
if doctype in ["Sales Invoice", "Sales Order"]: if doctype in ["Sales Invoice", "Sales Order"]:
@ -2224,6 +2247,28 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
return doclist return doclist
def get_received_items(reference_name, doctype, reference_fieldname):
target_doctypes = frappe.get_all(
doctype,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
as_list=True,
)
if target_doctypes:
target_doctypes = list(target_doctypes[0])
received_items_map = frappe._dict(
frappe.get_all(
doctype + " Item",
filters={"parent": ("in", target_doctypes)},
fields=[reference_fieldname, "qty"],
as_list=1,
)
)
return received_items_map
def set_purchase_references(doc): def set_purchase_references(doc):
# add internal PO or PR links if any # add internal PO or PR links if any
if doc.is_internal_transfer(): if doc.is_internal_transfer():

View File

@ -11,6 +11,7 @@ def get_data():
"Payment Request": "reference_name", "Payment Request": "reference_name",
"Sales Invoice": "return_against", "Sales Invoice": "return_against",
"Auto Repeat": "reference_document", "Auto Repeat": "reference_document",
"Purchase Invoice": "inter_company_invoice_reference",
}, },
"internal_links": { "internal_links": {
"Sales Order": ["items", "sales_order"], "Sales Order": ["items", "sales_order"],
@ -30,5 +31,6 @@ def get_data():
{"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]}, {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]},
{"label": _("Returns"), "items": ["Sales Invoice"]}, {"label": _("Returns"), "items": ["Sales Invoice"]},
{"label": _("Subscription"), "items": ["Auto Repeat"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]},
{"label": _("Internal Transfers"), "items": ["Purchase Invoice"]},
], ],
} }

View File

@ -2731,6 +2731,63 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si) einvoice = make_einvoice(si)
validate_totals(einvoice) validate_totals(einvoice)
def test_einvoice_discounts(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
# Normal Itemized Discount
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = ""
si.items[0].discount_amount = 4000
si.items[1].discount_amount = 300
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 300)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
# Invoice Discount on net total
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Net Total"
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
# Invoice Discount on grand total (Itemized Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48)
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
# Invoice Discount on grand total (Cash/Non-Trade Discount)
si = get_sales_invoice_for_e_invoice()
si.apply_discount_on = "Grand Total"
si.is_cash_or_non_trade_discount = 1
si.discount_amount = 400
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
def test_item_tax_net_range(self): def test_item_tax_net_range(self):
item = create_item("T Shirt") item = create_item("T Shirt")

View File

@ -855,8 +855,8 @@ def get_outstanding_invoices(
) )
for d in invoice_list: for d in invoice_list:
payment_amount = d.invoice_amount - d.outstanding payment_amount = d.invoice_amount_in_account_currency - d.outstanding_in_account_currency
outstanding_amount = d.outstanding outstanding_amount = d.outstanding_in_account_currency
if outstanding_amount > 0.5 / (10**precision): if outstanding_amount > 0.5 / (10**precision):
if ( if (
min_outstanding min_outstanding
@ -872,7 +872,7 @@ def get_outstanding_invoices(
"voucher_no": d.voucher_no, "voucher_no": d.voucher_no,
"voucher_type": d.voucher_type, "voucher_type": d.voucher_type,
"posting_date": d.posting_date, "posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount), "invoice_amount": flt(d.invoice_amount_in_account_currency),
"payment_amount": payment_amount, "payment_amount": payment_amount,
"outstanding_amount": outstanding_amount, "outstanding_amount": outstanding_amount,
"due_date": d.due_date, "due_date": d.due_date,
@ -1412,7 +1412,7 @@ def create_payment_ledger_entry(
if gle.against_voucher_type if gle.against_voucher_type
else gle.voucher_type, else gle.voucher_type,
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
"currency": gle.currency, "account_currency": gle.account_currency,
"amount": dr_or_cr, "amount": dr_or_cr,
"amount_in_account_currency": dr_or_cr_account_currency, "amount_in_account_currency": dr_or_cr_account_currency,
"delinked": True if cancel else False, "delinked": True if cancel else False,

View File

@ -500,6 +500,9 @@ class calculate_taxes_and_totals(object):
else: else:
self.doc.grand_total = flt(self.doc.net_total) self.doc.grand_total = flt(self.doc.net_total)
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt( self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
@ -594,6 +597,12 @@ class calculate_taxes_and_totals(object):
if not self.doc.apply_discount_on: if not self.doc.apply_discount_on:
frappe.throw(_("Please select Apply Discount On")) frappe.throw(_("Please select Apply Discount On"))
if self.doc.apply_discount_on == "Grand Total" and self.doc.get(
"is_cash_or_non_trade_discount"
):
self.discount_amount_applied = True
return
self.doc.base_discount_amount = flt( self.doc.base_discount_amount = flt(
self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount")
) )

View File

@ -624,7 +624,7 @@ class SalarySlip(TransactionBase):
data = self.get_data_for_eval() data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type): for struct_row in self._salary_structure_doc.get(component_type):
amount = self.eval_condition_and_formula(struct_row, data) amount = self.eval_condition_and_formula(struct_row, data)
if amount and struct_row.statistical_component == 0: if amount is not None and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, component_type) self.update_component_row(struct_row, amount, component_type)
def get_data_for_eval(self): def get_data_for_eval(self):
@ -854,6 +854,10 @@ class SalarySlip(TransactionBase):
component_row, joining_date, relieving_date component_row, joining_date, relieving_date
)[0] )[0]
# remove 0 valued components that have been updated later
if component_row.amount == 0:
self.remove(component_row)
def set_precision_for_component_amounts(self): def set_precision_for_component_amounts(self):
for component_type in ("earnings", "deductions"): for component_type in ("earnings", "deductions"):
for component_row in self.get(component_type): for component_row in self.get(component_type):

View File

@ -1108,10 +1108,26 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
} }
is_a_mapped_document(item) {
const mapped_item_field_map = {
"Delivery Note Item": ["si_detail", "so_detail", "dn_detail"],
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
};
const mappped_fields = mapped_item_field_map[item.doctype] || [];
return mappped_fields
.map((field) => item[field])
.filter(Boolean).length > 0;
}
batch_no(doc, cdt, cdn) { batch_no(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn); let item = frappe.get_doc(cdt, cdn);
if (!this.is_a_mapped_document(item)) {
this.apply_price_list(item, true); this.apply_price_list(item, true);
} }
}
toggle_conversion_factor(item) { toggle_conversion_factor(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same // toggle read only property for conversion factor field if the uom and stock uom are same
@ -1484,48 +1500,46 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
_set_values_for_item_list(children) { _set_values_for_item_list(children) {
var me = this; const items_rule_dict = {};
var items_rule_dict = {};
for(var i=0, l=children.length; i<l; i++) { for (const child of children) {
var d = children[i] ; const existing_pricing_rule = frappe.model.get_value(child.doctype, child.name, "pricing_rules");
let item_row = frappe.get_doc(d.doctype, d.name);
var existing_pricing_rule = frappe.model.get_value(d.doctype, d.name, "pricing_rules"); for (const [key, value] of Object.entries(child)) {
for(var k in d) { if (!["doctype", "name"].includes(key)) {
var v = d[k]; if (key === "price_list_rate") {
if (["doctype", "name"].indexOf(k)===-1) { frappe.model.set_value(child.doctype, child.name, "rate", value);
if(k=="price_list_rate") {
item_row['rate'] = v;
} }
if (k !== 'free_item_data') { if (key !== "free_item_data") {
item_row[k] = v; frappe.model.set_value(child.doctype, child.name, key, value);
} }
} }
} }
frappe.model.round_floats_in(item_row, ["price_list_rate", "discount_percentage"]); frappe.model.round_floats_in(
frappe.get_doc(child.doctype, child.name),
["price_list_rate", "discount_percentage"],
);
// if pricing rule set as blank from an existing value, apply price_list // if pricing rule set as blank from an existing value, apply price_list
if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) { if (!this.frm.doc.ignore_pricing_rule && existing_pricing_rule && !child.pricing_rules) {
me.apply_price_list(frappe.get_doc(d.doctype, d.name)); this.apply_price_list(frappe.get_doc(child.doctype, child.name));
} else if(!d.pricing_rules) { } else if (!child.pricing_rules) {
me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name)); this.remove_pricing_rule(frappe.get_doc(child.doctype, child.name));
} }
if (d.free_item_data.length > 0) { if (child.free_item_data.length > 0) {
me.apply_product_discount(d); this.apply_product_discount(child);
} }
if (d.apply_rule_on_other_items) { if (child.apply_rule_on_other_items) {
items_rule_dict[d.name] = d; items_rule_dict[child.name] = child;
} }
} }
me.frm.refresh_field('items'); this.apply_rule_on_other_items(items_rule_dict);
me.apply_rule_on_other_items(items_rule_dict); this.calculate_taxes_and_totals();
me.calculate_taxes_and_totals();
} }
apply_rule_on_other_items(args) { apply_rule_on_other_items(args) {

View File

@ -271,14 +271,14 @@ def get_item_list(invoice):
item.description = sanitize_for_json(d.item_name) item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty) item.qty = abs(item.qty)
if flt(item.qty) != 0.0:
item.unit_rate = abs(item.taxable_value / item.qty)
else:
item.unit_rate = abs(item.taxable_value)
item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
if invoice.get("apply_discount_on"):
item.discount_amount = item.base_amount - item.base_net_amount
item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty
item.gross_amount = abs(item.taxable_value) + item.discount_amount
item.taxable_value = abs(item.taxable_value)
item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N" item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
item.serial_no = "" item.serial_no = ""
@ -352,6 +352,13 @@ def update_item_taxes(invoice, item):
def get_invoice_value_details(invoice): def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict()) invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")])) invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")]))
if (
invoice.apply_discount_on == "Grand Total"
and invoice.discount_amount
and invoice.get("is_cash_or_non_trade_discount")
):
invoice_value_details.invoice_discount_amt = invoice.discount_amount
else:
invoice_value_details.invoice_discount_amt = 0 invoice_value_details.invoice_discount_amt = 0
invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.round_off = invoice.base_rounding_adjustment

View File

@ -1060,6 +1060,14 @@ def update_taxable_values(doc, method):
considered_rows.append(prev_row_id) considered_rows.append(prev_row_id)
for item in doc.get("items"): for item in doc.get("items"):
if (
doc.apply_discount_on == "Grand Total"
and doc.discount_amount
and doc.get("is_cash_or_non_trade_discount")
):
proportionate_value = item.base_amount if doc.base_total else item.qty
total_value = doc.base_total if doc.base_total else doc.total_qty
else:
proportionate_value = item.base_net_amount if doc.base_net_total else item.qty proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
total_value = doc.base_net_total if doc.base_net_total else doc.total_qty total_value = doc.base_net_total if doc.base_net_total else doc.total_qty

View File

@ -219,8 +219,8 @@ erpnext.company.setup_queries = function(frm) {
["default_discount_account", {}], ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}], ["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}], ["discount_received_account", {"root_type": "Income"}],
["exchange_gain_loss_account", {"root_type": "Expense"}], ["exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}],
["unrealized_exchange_gain_loss_account", {"root_type": "Expense"}], ["unrealized_exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}],
["accumulated_depreciation_account", ["accumulated_depreciation_account",
{"root_type": "Asset", "account_type": "Accumulated Depreciation"}], {"root_type": "Asset", "account_type": "Accumulated Depreciation"}],
["depreciation_expense_account", {"root_type": "Expense", "account_type": "Depreciation"}], ["depreciation_expense_account", {"root_type": "Expense", "account_type": "Depreciation"}],

View File

@ -14,7 +14,6 @@ def boot_session(bootinfo):
if frappe.session["user"] != "Guest": if frappe.session["user"] != "Guest":
update_page_info(bootinfo) update_page_info(bootinfo)
load_country_and_currency(bootinfo)
bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory")
bootinfo.sysdefaults.customer_group = frappe.db.get_single_value( bootinfo.sysdefaults.customer_group = frappe.db.get_single_value(
"Selling Settings", "customer_group" "Selling Settings", "customer_group"
@ -53,20 +52,6 @@ def boot_session(bootinfo):
bootinfo.party_account_types = frappe._dict(party_account_types) bootinfo.party_account_types = frappe._dict(party_account_types)
def load_country_and_currency(bootinfo):
country = frappe.db.get_default("country")
if country and frappe.db.exists("Country", country):
bootinfo.docs += [frappe.get_doc("Country", country)]
bootinfo.docs += frappe.db.sql(
"""select name, fraction, fraction_units,
number_format, smallest_currency_fraction_value, symbol from tabCurrency
where enabled=1""",
as_dict=1,
update={"doctype": ":Currency"},
)
def update_page_info(bootinfo): def update_page_info(bootinfo):
bootinfo.page_info.update( bootinfo.page_info.update(
{ {

View File

@ -501,6 +501,7 @@ class PurchaseReceipt(BuyingController):
and not d.is_fixed_asset and not d.is_fixed_asset
and flt(d.qty) and flt(d.qty)
and provisional_accounting_for_non_stock_items and provisional_accounting_for_non_stock_items
and d.get("provisional_expense_account")
): ):
self.add_provisional_gl_entry( self.add_provisional_gl_entry(
d, gl_entries, self.posting_date, d.get("provisional_expense_account") d, gl_entries, self.posting_date, d.get("provisional_expense_account")

View File

@ -1,3 +1,35 @@
[project]
name = "erpnext"
authors = [
{ name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"}
]
description = "Open Source ERP"
requires-python = ">=3.10"
readme = "README.md"
dynamic = ["version"]
dependencies = [
# Core dependencies
"pycountry~=20.7.3",
"python-stdnum~=1.16",
"Unidecode~=1.2.0",
"redisearch~=2.1.0",
# integration dependencies
"gocardless-pro~=1.22.0",
"googlemaps",
"plaid-python~=7.2.1",
"python-youtube~=0.8.0",
"taxjar~=1.9.2",
"tweepy~=3.10.0",
]
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
[tool.bench.dev-dependencies]
hypothesis = "~=6.31.0"
[tool.black] [tool.black]
line-length = 99 line-length = 99

View File

@ -1,11 +0,0 @@
# frappe # https://github.com/frappe/frappe is installed during bench-init
gocardless-pro~=1.22.0
googlemaps
plaid-python~=7.2.1
pycountry~=20.7.3
python-stdnum~=1.16
python-youtube~=0.8.0
taxjar~=1.9.2
tweepy~=3.10.0
Unidecode~=1.2.0
redisearch~=2.1.0

View File

@ -1,23 +1,6 @@
from setuptools import setup, find_packages # TODO: Remove this file when v15.0.0 is released
import re, ast from setuptools import setup
# get version from __version__ variable in erpnext/__init__.py name = "frappe"
_version_re = re.compile(r"__version__\s+=\s+(.*)")
with open("requirements.txt") as f: setup()
install_requires = f.read().strip().split("\n")
with open("erpnext/__init__.py", "rb") as f:
version = str(ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)))
setup(
name="erpnext",
version=version,
description="Open Source ERP",
author="Frappe Technologies",
author_email="info@erpnext.com",
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=install_requires,
)